cozy-labs/cozy-desktop

View on GitHub
core/remote/document.js

Summary

Maintainability
B
6 hrs
Test Coverage
/** The remote Cozy metadata, as returned by cozy-client-js.
 *
 * @module core/remote/document
 * @flow
 */

const posixPath = require('path').posix

const {
  FILE_TYPE,
  DIR_TYPE,
  ROOT_DIR_ID,
  TRASH_DIR_ID,
  TRASH_DIR_NAME
} = require('./constants')

/*::
import type {
  FILE_TYPE as FILE,
  DIR_TYPE as DIR,
  VERSIONS_DOCTYPE,
} from './constants'

// ('contents') => Array<JsonApiRef>
// ('file') => ?JsonApiRef
// ('old_versions') => Array<JsonApiFileVersion>
// ('referenced_by') => Array<JsonApiRef>
export type RemoteRelations = any => any

export type RemoteFileAttributes = {|
  type: FILE,
  class: string,
  executable?: boolean,
  md5sum: string,
  mime: string,
  size: string,
  trashed: boolean,
|}

export type RemoteDirAttributes = {|
  type: DIR,
  path: string,
  not_synchronized_on?: Array<{
    id: string,
    type: string
  }>
|}

export type RemoteBase = {|
  _id: string,
  _rev: string,
  dir_id: string,
  name: string,
  tags: string[],
  created_at: string,
  updated_at: string,
  cozyMetadata?: Object,
  metadata?: Object,
  restore_path?: string,
  relations: RemoteRelations
|}
export type RemoteFile = {| ...RemoteBase, ...RemoteFileAttributes |}
export type FullRemoteFile = {| ...RemoteFile, path: string |}
export type RemoteDir = {| ...RemoteBase, ...RemoteDirAttributes |}
export type RemoteDoc = RemoteFile|RemoteDir

export type RemoteFileVersion = {|
  _id: string,
  _rev: string,
  _type: VERSIONS_DOCTYPE,
  cozyMetadata: Object,
  md5sum: string,
  metadata?: Object,
  relationships: JsonApiRelationShips,
  size: string,
  tags: string[],
  updated_at: string,
|}

export type CouchDBChange = {|
  id: string,
  seq: string,
  doc: CouchDBDoc|CouchDBDeletion,
  changes: $ReadOnlyArray<{| rev: string |}>
|}
type CommonCouchDBAttributes = {|
  _id: string,
  _rev: string,
  cozyMetadata: Object,
  created_at: string,
  dir_id: string,
  metadata?: Object,
  name: string,
  path: string,
  restore_path?: string,
  updated_at: string,
  tags: string[],
|}
export type CouchDBDir = {|
  ...CommonCouchDBAttributes,
  type: DIR,
  not_synchronized_on?: Array<{
    id: string,
    type: string
  }>,
|}
export type CouchDBFile = {|
  ...CommonCouchDBAttributes,
  type: FILE,
  class: string,
  executable?: boolean,
  md5sum: string,
  mime: string,
  size: string,
  trashed: boolean,
|}
export type CouchDBDoc = CouchDBDir | CouchDBFile
export type CouchDBDeletion = {|
  _id: string,
  _rev: string,
  _deleted: true
|}

export type JsonApiFileAttributes = {|
  type: FILE,
  class?: string, // file only
  dir_id?: string,
  executable?: boolean,
  md5sum?: string,
  mime?: string,
  name?: string,
  size?: string, // file only
  tags?: string[],
  trashed: boolean,
  created_at: string,
  updated_at: string,
  cozyMetadata?: Object,
  metadata?: Object,
  restore_path?: string,
|}

export type JsonApiDirAttributes = {|
  type: DIR,
  dir_id?: string,
  name?: string,
  path?: string, // folder only
  tags?: string[],
  created_at: string,
  updated_at: string,
  cozyMetadata?: Object,
  metadata?: Object,
  restore_path?: string,
|}

// Old cozy-client-js responses type
export type RemoteJsonFile = {|
  _id: string,
  _rev: string,
  _type: string,
  attributes: JsonApiFileAttributes,
  relations?: RemoteRelations,
  relationships: JsonApiRelationShips,
|}
export type RemoteJsonDir = {|
  _id: string,
  _rev: string,
  _type: string,
  attributes: JsonApiDirAttributes,
  relations?: RemoteRelations,
  relationships: JsonApiRelationShips,
|}
export type RemoteJsonDoc = RemoteJsonFile|RemoteJsonDir

// New cozy-client responses types
type JsonApiRef = {
  id: string,
  type: string,
}

type JsonApiFileVersionAttributes = {|
  md5sum: string,
  size: string,
  tags: string[],
  updated_at: string,
|}

export type JsonApiFileVersion = {|
  _id: string,
  _rev: string,
  _type: VERSIONS_DOCTYPE,
  attributes: JsonApiFileVersionAttributes,
  cozyMetadata: Object,
  metadata?: Object,
  relationships: JsonApiRelationShips,
|}

type JsonApiRelationShips = {|
  contents?: { data?: JsonApiRef[] },
  file?: { data: JsonApiRef },
  old_versions?: { data?: JsonApiFileVersion[] },
  referenced_by?: { data?: JsonApiRef | JsonApiRef[] },
|}


type JsonApiDeletion = {|
  id: string,
  rev: string,
  _deleted: true
|}

type JsonApiDoc =
  {|
    id: string,
    type: string,
    meta?: {
      rev?: string
    },
    links: Object,
    attributes: JsonApiFileAttributes|JsonApiDirAttributes,
    relationships: JsonApiRelationShips
  |}
  | JsonApiDeletion
*/

module.exports = {
  specialId,
  dropSpecialDocs,
  inRemoteTrash,
  trashedDoc,
  withDefaultValues,
  remoteJsonToRemoteDoc,
  jsonApiToRemoteJsonDoc,
  jsonFileVersionToRemoteFileVersion
}

function isFile(
  doc /*: { type: DIR } | { type: FILE } */
) /*: boolean %checks */ {
  return doc.type === FILE_TYPE
}

function specialId(id /*: string */) /*: boolean %checks */ {
  return id === ROOT_DIR_ID || id === TRASH_DIR_ID || id.startsWith('_design/')
}

function dropSpecialDocs /*::<T: $ReadOnlyArray<{ _id: string }>>*/(
  docs /*: T */
) /*: T */ {
  return docs.filter(doc => !specialId(doc._id))
}

function inRemoteTrash(
  doc /*: { type: FILE, trashed: boolean } | { type: DIR, path: string } */
) /*: boolean %checks */ {
  return isFile(doc) ? doc.trashed : doc.path.startsWith(`/${TRASH_DIR_NAME}/`)
}

function trashedDoc /*::<T: { type: FILE, trashed: boolean } | { type: DIR, path: string } > */(
  doc /*: T */
) /*: T */ {
  return inRemoteTrash(doc)
    ? doc
    : isFile(doc)
    ? { ...doc, trashed: true }
    : { ...doc, path: posixPath.join('/', TRASH_DIR_NAME, doc.path) }
}

// The following attributes can be omitted by cozy-stack if not defined
function withDefaultValues /*:: <T: JsonApiDirAttributes|JsonApiFileAttributes|JsonApiFileVersionAttributes> */(
  attributes /*: T */
) /*: T */ {
  if (attributes.type) {
    if (attributes.type === DIR_TYPE) {
      return {
        ...attributes,
        dir_id: attributes.dir_id || '',
        name: attributes.name || '',
        path: attributes.path || '',
        tags: attributes.tags || []
      }
    } else {
      return {
        ...attributes,
        class: attributes.class || 'application',
        dir_id: attributes.dir_id || '',
        md5sum: attributes.md5sum || '',
        mime: attributes.mime || 'application/octet-stream',
        name: attributes.name || '',
        tags: attributes.tags || []
      }
    }
  } else {
    return {
      ...attributes,
      md5sum: attributes.md5sum || '',
      size: attributes.size || '0',
      tags: attributes.tags || []
    }
  }
}

// It appears the `relations` attribute is not always added to remote documents
// by `cozy-client-js` so we use a function always returning an empty array in
// this case.
// Also, when a remote file has never been modified, its `old_versions` relation
// will return `undefined` instead of an empty Array so we'll default the
// returned value to an empty Array instead.
function withDefaultRelations /*::<T: ?RemoteRelations> */(
  relations /*: T */
) /*: { relations: RemoteRelations } */ {
  if (relations != null) {
    const originalRelations = relations
    return {
      relations: relation => originalRelations(relation) || []
    }
  } else {
    return {
      relations: () => []
    }
  }
}

function remoteJsonToRemoteDoc /*:: <T: RemoteJsonDoc> */(
  json /*: T */
) /*: RemoteDoc */ {
  if (json.attributes.type === DIR_TYPE) {
    const remoteDir = ({
      type: DIR_TYPE,
      _id: json._id,
      _rev: json._rev,
      ...withDefaultValues(json.attributes),
      ...withDefaultRelations(json.relations)
    } /*: RemoteDir */)

    return remoteDir
  } else {
    const remoteFile = ({
      type: FILE_TYPE,
      _id: json._id,
      _rev: json._rev,
      ...withDefaultValues(json.attributes),
      ...withDefaultRelations(json.relations)
    } /*: RemoteFile */)

    return remoteFile
  }
}

function jsonApiToRemoteJsonDoc(
  json /*: JsonApiDoc */
) /*: RemoteJsonDoc|CouchDBDeletion */ {
  if (json._deleted) {
    return ({
      _id: json.id,
      _rev: json.rev,
      _deleted: true
    } /*: CouchDBDeletion */)
  } else if (!json.meta || !json.meta.rev) {
    const error = new Error('Missing meta.rev attribute in JsonAPI resource.')
    // $FlowFixMe we add the `data` attribute on purpose
    error.data = { json }
    throw error
  } else {
    const { attributes, id, type, relationships } = json

    return attributes.type === DIR_TYPE
      ? ({
          _id: id,
          _type: type,
          _rev: json.meta.rev,
          attributes,
          relationships
        } /*: RemoteJsonDir */)
      : ({
          _id: id,
          _type: type,
          _rev: json.meta.rev,
          attributes,
          relationships
        } /*: RemoteJsonFile */)
  }
}

function jsonFileVersionToRemoteFileVersion(
  version /*: JsonApiFileVersion */
) /*: RemoteFileVersion */ {
  return {
    _id: version._id,
    _rev: version._rev,
    _type: version._type,
    cozyMetadata: version.cozyMetadata,
    metadata: version.metadata,
    relationships: version.relationships,
    ...withDefaultValues(version.attributes)
  }
}