Skip to content
Snippets Groups Projects
migration.ts 5.55 KiB
Newer Older
  • Learn to ignore specific revisions
  • Bastien DUMONT's avatar
    Bastien DUMONT committed
    import * as Sentry from '@sentry/react'
    
    import { Client, Q, QueryDefinition, QueryResult } from 'cozy-client'
    
    import { SCHEMAS_DOCTYPE } from 'doctypes'
    import { Schema } from 'models'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import logApp from 'utils/logger'
    
    import {
      MIGRATION_RESULT_COMPLETE,
      MIGRATION_RESULT_FAILED,
      MIGRATION_RESULT_NOOP,
      SCHEMA_INITIAL_VERSION,
    } from './migration.data'
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    import {
      Migration,
      MigrationQueryOptions,
      MigrationResult,
    } from './migration.type'
    
    
    function migrationNoop(): MigrationResult {
      return { type: MIGRATION_RESULT_NOOP, errors: [] }
    }
    
    /**
     * Return schema version
     * @param _client cozyClient
     * @returns Promise<number> Version number of schema
     */
    async function currentSchemaVersion(_client: Client): Promise<number> {
    
      const query = Q(SCHEMAS_DOCTYPE)
    
      const data: QueryResult<Schema[]> = await _client.query(query.limitBy(1))
      return data.data[0].version
    }
    
    /**
     * Retrieve all documents of a given doctype
     * @param _client cozyClient
     * @returns all documents of given doctype
     */
    
    async function getDocs(
      _client: Client,
      doctype: string,
      options?: MigrationQueryOptions
    ): Promise<any> {
      let query: QueryDefinition
    
      if (options?.scope === 'conso') {
    
        query = Q(doctype)
          .where({})
          .indexFields(['year', 'month', 'day'])
          .sortBy([{ year: 'desc' }, { month: 'desc' }, { day: 'desc' }])
          .limitBy(options.limit)
      } else {
        query = Q(doctype)
      }
    
    
      const data: QueryResult<any[]> = await _client.query(query)
      return data.data
    }
    
    /**
     * Update schema version
     */
    async function updateSchemaVersion(
      _client: Client,
      targetSchemaVersion: number
    ): Promise<void> {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      logApp.info('[Migration] Update schema version')
    
      const query = Q(SCHEMAS_DOCTYPE)
    
      const data: QueryResult<Schema[]> = await _client.query(query.limitBy(1))
      const doc = data.data[0]
      doc.version = targetSchemaVersion
      await _client.save(doc)
    }
    
    /**
     * Save updated docs
     * @returns Promise<MigrationResult>
     */
    
    // eslint-disable-next-line @typescript-eslint/require-await
    
    async function save(_client: Client, docs: any[]): Promise<MigrationResult> {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      logApp.info('[Migration] Saving docs...')
    
      const migrationResult = migrationNoop()
    
    
      if (docs.length) {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        logApp.info('[Migration] Saving docs...')
    
        docs.forEach(async doc => {
    
          if (doc.deleteAction) {
            await _client.destroy(doc)
    
          } else if (doc.createAction) {
            await _client.create(doc.doctype, doc)
    
          } else {
            await _client.save(doc)
          }
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        logApp.info('[Migration] Docs saved')
    
        migrationResult.type = migrationResult.errors.length
          ? MIGRATION_RESULT_FAILED
          : MIGRATION_RESULT_COMPLETE
      }
    
      return migrationResult
    }
    
    /**
     * Does schema doctype exist
     * @param _client cozyClient
     * @returns Promise<number> Version number of schema
     */
    const schemaExist = async (_client: Client): Promise<boolean> => {
    
      const query = Q(SCHEMAS_DOCTYPE)
    
      const data: QueryResult<Schema[]> = await _client.query(query.limitBy(1))
      return data.data.length > 0 ? true : false
    }
    
    
    export const initSchemaDoctype = async (
      _client: Client,
      targetVersion = SCHEMA_INITIAL_VERSION
    ) => {
      logApp.info(`[Migration] Init schema doctype to version ${targetVersion}`)
    
      await _client.create(SCHEMAS_DOCTYPE, {
    
        version: targetVersion,
    
      })
    }
    
    /**
     * Run migration
     */
    export async function migrate(
      migration: Migration,
      _client: Client
    ): Promise<MigrationResult> {
    
      if (migration.isEmpty) {
        updateSchemaVersion(_client, migration.targetSchemaVersion)
        return {
          errors: [],
          type: 'MigrationComplete',
        }
      }
    
      if (!(await schemaExist(_client))) {
        await initSchemaDoctype(_client)
      }
      if ((await currentSchemaVersion(_client)) !== migration.baseSchemaVersion) {
        return migrationNoop()
      } else {
        let result: MigrationResult
        try {
    
          const docToUpdate = await getDocs(
    
          if (migration.isDeprecated) {
            result = migrationNoop()
          } else if (docToUpdate.length && !migration.isCreate) {
    
            const migratedDocs = migration.run(_client, docToUpdate)
    
            if (migratedDocs.length) {
              result = await save(_client, migratedDocs)
            } else {
              result = migrationNoop()
            }
    
          } else {
            result = migrationNoop()
          }
    
    
          if (migration.isCreate && !migration.isDeprecated) {
    
            migration.run(_client, docToUpdate)
    
            result = { type: MIGRATION_RESULT_COMPLETE, errors: [] }
          }
    
    
          switch (result.type) {
    
            case MIGRATION_RESULT_FAILED:
              throw new Error('Migration failed')
    
            case MIGRATION_RESULT_NOOP:
            case MIGRATION_RESULT_COMPLETE:
              await updateSchemaVersion(_client, migration.targetSchemaVersion)
              break
          }
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        } catch (error: any) {
          console.error(error)
    
          Sentry.captureException(error)
    
          result = {
            type: MIGRATION_RESULT_FAILED,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            errors: [error.toString()],
    
          }
        }
    
        return result
      }
    }
    
    /**
     * Handle migration logging
     * @param migration Migration
     * @param result MigratioNResult
     * @returns string
     */
    export function migrationLog(
      migration: Migration,
      result: MigrationResult
    ): string {
      let globalResult
    
      switch (result.type) {
        case MIGRATION_RESULT_NOOP:
          globalResult = 'NOOP'
          break
        case MIGRATION_RESULT_COMPLETE:
          globalResult = 'Complete'
          break
        case MIGRATION_RESULT_FAILED:
          globalResult = 'Failed'
          break
        default:
          globalResult = 'Unexpected error'
      }
      return `--- ${migration.description} => ${globalResult}`
    }