huridocs/uwazi

View on GitHub
app/shared/types/settingsSchema.ts

Summary

Maintainability
C
1 day
Test Coverage
A
95%
import Ajv, { ErrorObject } from 'ajv';
import { wrapValidator } from 'shared/tsUtils';
import { objectIdSchema, languagesListSchema, geolocationSchema } from 'shared/types/commonSchemas';
import { OnlineRelationshipPropertyUpdateStrategy } from 'api/relationships.v2/services/propertyUpdateStrategies/OnlineRelationshipPropertyUpdateStrategy';
import { QueuedRelationshipPropertyUpdateStrategy } from 'api/relationships.v2/services/propertyUpdateStrategies/QueuedRelationshipPropertyUpdateStrategy';
import { Settings } from './settingsType';

const emitSchemaTypes = true;

const ajv = new Ajv({ allErrors: true });
ajv.addVocabulary(['tsType']);

ajv.addKeyword({
  keyword: 'hasDefaultLanguage',
  errors: true,
  type: 'object',
  validate(schema: boolean, settings: Settings) {
    const errors: ErrorObject[] = [];
    const { languages = [] } = settings;
    const defaultLanguage = languages.filter(language => language.default === true);

    if (languages.length > 0 && defaultLanguage.length === 0) {
      errors.push({
        keyword: 'hasDefaultLanguage',
        schemaPath: '',
        params: { keyword: 'hasDefaultLanguage', schema },
        message: 'At least one language must be selected as default',
        instancePath: 'settings.languages',
      });
    }

    if (defaultLanguage.length > 1) {
      errors.push({
        keyword: 'hasDefaultLanguage',
        schemaPath: '',
        params: { keyword: 'hasDefaultLanguage', schema },
        message: 'Only one language must be selected as default',
        instancePath: 'settings.languages',
      });
    }

    if (errors.length) {
      throw new Ajv.ValidationError(errors);
    }

    return true;
  },
});

ajv.addKeyword({
  keyword: 'linkTypeLinksShouldHaveUrl',
  errors: true,
  type: 'object',
  validate(schema: boolean, settings: Settings) {
    const errors: ErrorObject[] = [];
    const { links = [] } = settings;

    links.forEach((link, index) => {
      if (link.type === 'link' && !link.url) {
        errors.push({
          keyword: 'linkTypeLinksShouldHaveUrl',
          schemaPath: '',
          params: { keyword: 'linkTypeLinksShouldHaveUrl', schema },
          message: 'Links of type link should have url',
          instancePath: `/links/${index}`,
        });
      }
    });

    if (errors.length) {
      throw new Ajv.ValidationError(errors);
    }

    return true;
  },
});

ajv.addKeyword({
  keyword: 'groupTypeLinksShouldNotHaveUrl',
  errors: true,
  type: 'object',
  validate(schema: boolean, settings: Settings) {
    const errors: ErrorObject[] = [];
    const { links = [] } = settings;

    links.forEach((link, index) => {
      if (link.type === 'group' && link.url) {
        errors.push({
          keyword: 'groupTypeLinksShouldNotHaveUrl',
          schemaPath: '',
          params: { keyword: 'groupTypeLinksShouldNotHaveUrl', schema },
          message: 'Links of type group should not have url',
          instancePath: `/links/${index}`,
        });
      }
    });

    if (errors.length) {
      throw new Ajv.ValidationError(errors);
    }

    return true;
  },
});

ajv.addKeyword({
  keyword: 'linkTypeLinksShouldNotHaveSublinks',
  errors: true,
  type: 'object',
  validate(schema: boolean, settings: Settings) {
    const errors: ErrorObject[] = [];
    const { links = [] } = settings;

    links.forEach((link, index) => {
      if (link.type === 'link' && link.sublinks?.length) {
        errors.push({
          keyword: 'linkTypeLinksShouldNotHaveSublinks',
          schemaPath: '',
          params: { keyword: 'linkTypeLinksShouldNotHaveSublinks', schema },
          message: 'Links of type link should not have sublinks',
          instancePath: `/links/${index}`,
        });
      }
    });

    if (errors.length) {
      throw new Ajv.ValidationError(errors);
    }

    return true;
  },
});

ajv.addKeyword({
  keyword: 'groupTypeLinksShouldHaveSublinks',
  errors: true,
  type: 'object',
  validate(schema: boolean, settings: Settings) {
    const errors: ErrorObject[] = [];
    const { links = [] } = settings;

    links.forEach((link, index) => {
      if (link.type === 'group' && !link.sublinks) {
        errors.push({
          keyword: 'groupTypeLinksShouldHaveSublinks',
          schemaPath: '',
          params: { keyword: 'groupTypeLinksShouldHaveSublinks', schema },
          message: 'Links of type group should have sublinks',
          instancePath: `/links/${index}`,
        });
      }
    });

    if (errors.length) {
      throw new Ajv.ValidationError(errors);
    }

    return true;
  },
});

const automaticTranslationSchema = {
  title: 'AutomaticTranslationConfig',
  type: 'object',
  additionalProperties: false,
  required: ['active'],
  properties: {
    active: { type: 'boolean' },
    templates: {
      type: 'array',
      items: {
        type: 'object',
        additionalProperties: false,
        properties: {
          template: { type: 'string' },
          commonProperties: { type: 'array', items: { type: 'string' } },
          properties: { type: 'array', items: { type: 'string' } },
        },
        required: ['template'],
      },
    },
  },
};

const itemSchema = {
  type: 'object',
  additionalProperties: false,
  properties: {
    id: { type: 'string' },
    name: { type: 'string' },
  },
};

const settingsFilterSchema = {
  type: 'object',
  additionalProperties: false,
  definitions: { objectIdSchema },
  properties: {
    _id: objectIdSchema,
    id: { type: 'string' },
    name: { type: 'string' },
    items: { type: 'array', items: itemSchema },
  },
};

const settingsSyncTemplateSchema = {
  type: 'object',
  required: ['properties'],
  properties: {
    properties: { type: 'array', items: { type: 'string' } },
    filter: { type: 'string' },
    attachments: { type: 'boolean' },
  },
  additionalProperties: false,
};

const settingsSyncRelationtypesSchema = {
  type: 'array',
  items: { type: 'string' },
};

const settingsSyncSchema = {
  type: 'object',
  additionalProperties: false,
  required: ['url', 'username', 'password', 'name', 'config'],
  properties: {
    url: { type: 'string' },
    active: { type: 'boolean' },
    username: { type: 'string' },
    password: { type: 'string' },
    name: { type: 'string' },
    config: {
      type: 'object',
      properties: {
        templates: {
          type: 'object',
          additionalProperties: settingsSyncTemplateSchema,
        },
        relationtypes: settingsSyncRelationtypesSchema,
      },
      additionalProperties: false,
    },
  },
};

const settingsPreserveConfigSchema = {
  title: 'PreserveConfig',
  type: 'object',
  additionalProperties: false,
  definitions: { objectIdSchema },
  required: ['host', 'config', 'masterToken'],
  properties: {
    host: { type: 'string' },
    masterToken: { type: 'string' },
    config: {
      type: 'array',
      items: {
        required: ['token', 'template'],
        type: 'object',
        additionalProperties: false,
        properties: { token: { type: 'string' }, template: objectIdSchema, user: objectIdSchema },
      },
    },
  },
};

const settingsSublinkSchema = {
  type: 'object',
  additionalProperties: false,
  properties: {
    title: { type: 'string' },
    type: { type: 'string', enum: ['link'] },
    url: { type: 'string' },
    localId: { type: 'string' },
  },
  required: ['title', 'url', 'type'],
};

const settingsLinkSchema = {
  type: 'object',
  additionalProperties: false,
  definitions: { objectIdSchema, settingsSublinkSchema },
  properties: {
    _id: objectIdSchema,
    title: { type: 'string' },
    url: { type: 'string' },
    localId: { type: 'string' },
    sublinks: {
      type: 'array',
      items: settingsSublinkSchema,
    },
    type: { type: 'string', enum: ['link', 'group'] },
  },
  required: ['title', 'type'],
};

const settingsSchema = {
  $schema: 'http://json-schema.org/schema#',
  $async: true,
  type: 'object',
  title: 'Settings',
  definitions: {
    objectIdSchema,
    languagesListSchema,
    settingsFilterSchema,
    settingsLinkSchema,
    settingsSyncSchema,
    settingsPreserveConfigSchema,
    automaticTranslationSchema,
  },
  additionalProperties: false,
  hasDefaultLanguage: true,
  linkTypeLinksShouldHaveUrl: true,
  groupTypeLinksShouldNotHaveUrl: true,
  linkTypeLinksShouldNotHaveSublinks: true,
  groupTypeLinksShouldHaveSublinks: true,
  properties: {
    _id: objectIdSchema,
    __v: { type: 'number' },
    project: { type: 'string' },
    site_name: { type: 'string' },
    favicon: { type: 'string' },
    contactEmail: { type: 'string' },
    senderEmail: { type: 'string' },
    home_page: { type: 'string' },
    defaultLibraryView: { type: 'string' },
    private: { type: 'boolean' },
    allowcustomJS: { type: 'boolean' },
    openPublicEndpoint: { type: 'boolean' },
    cookiepolicy: { type: 'boolean' },
    mailerConfig: { type: 'string' },
    publicFormDestination: { type: 'string' },
    allowedPublicTemplates: {
      type: 'array',
      items: { type: 'string' },
    },
    analyticsTrackingId: { type: 'string' },
    matomoConfig: { type: 'string' },
    dateFormat: { type: 'string' },
    custom: { oneOf: [{ type: 'string' }, { type: 'object' }] },
    customCSS: { type: 'string' },
    customJS: { type: 'string' },
    mapApiKey: { type: 'string', pattern: '^[a-zA-Z0-9._]*$' },
    mapLayers: { type: 'array', minItems: 1, items: { type: 'string' } },
    newNameGeneration: { type: 'boolean', enum: [true] },
    ocrServiceEnabled: { type: 'boolean' },

    sync: { type: 'array', items: settingsSyncSchema },

    languages: languagesListSchema,

    filters: {
      type: 'array',
      items: settingsFilterSchema,
    },

    links: {
      type: 'array',
      items: settingsLinkSchema,
    },

    features: {
      type: 'object',
      properties: {
        _id: { type: 'string' },
        tocGeneration: {
          type: 'object',
          required: ['url'],
          additionalProperties: false,
          properties: {
            url: { type: 'string' },
          },
        },
        topicClassification: { type: 'boolean' },
        favorites: { type: 'boolean' },
        preserve: settingsPreserveConfigSchema,
        convertToPdf: {
          type: 'object',
          additionalProperties: false,
          required: ['url', 'active'],
          properties: {
            active: { type: 'boolean' },
            url: { type: 'string' },
          },
        },
        ocr: {
          type: 'object',
          additionalProperties: false,
          required: ['url'],
          properties: {
            url: { type: 'string' },
          },
        },
        segmentation: {
          type: 'object',
          additionalProperties: false,
          required: ['url'],
          properties: {
            url: { type: 'string' },
          },
        },
        twitterIntegration: {
          type: 'object',
          additionalProperties: false,
          required: [
            'searchQueries',
            'hashtagsTemplateName',
            'tweetsTemplateName',
            'language',
            'tweetsLanguages',
          ],
          properties: {
            searchQueries: { type: 'array', items: { type: 'string' } },
            hashtagsTemplateName: { type: 'string' },
            tweetsTemplateName: { type: 'string' },
            language: { type: 'string' },
            tweetsLanguages: { type: 'array', items: { type: 'string' } },
          },
        },
        metadataExtraction: {
          type: 'object',
          additionalProperties: false,
          required: ['url'],
          properties: {
            url: { type: 'string' },
            templates: {
              type: 'array',
              items: {
                type: 'object',
                additionalProperties: false,
                required: ['template', 'properties'],
                properties: {
                  template: objectIdSchema,
                  properties: { type: 'array', items: { type: 'string' } },
                },
              },
            },
          },
        },
        newRelationships: {
          oneOf: [
            { type: 'boolean' },
            {
              type: 'object',
              additionalProperties: false,
              required: ['updateStrategy'],
              properties: {
                updateStrategy: {
                  type: 'string',
                  enum: [
                    OnlineRelationshipPropertyUpdateStrategy.name,
                    QueuedRelationshipPropertyUpdateStrategy.name,
                  ],
                },
              },
            },
          ],
        },
        automaticTranslation: automaticTranslationSchema,
      },
    },
    mapStartingPoint: geolocationSchema,
    tilesProvider: { type: 'string' },
  },
};

const validateSettings = wrapValidator(ajv.compile(settingsSchema));

export {
  validateSettings,
  emitSchemaTypes,
  itemSchema,
  settingsFilterSchema,
  settingsSyncTemplateSchema,
  settingsSyncRelationtypesSchema,
  settingsSyncSchema,
  settingsSublinkSchema,
  settingsLinkSchema,
  settingsSchema,
  settingsPreserveConfigSchema,
  automaticTranslationSchema,
};