mikro-orm/mikro-orm

View on GitHub
packages/core/src/platforms/Platform.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import { clone } from '../utils/clone';
import { EntityRepository } from '../entity';
import { UnderscoreNamingStrategy, type NamingStrategy } from '../naming-strategy';
import type { Constructor, EntityProperty, IPrimaryKey, ISchemaGenerator, PopulateOptions, Primary, EntityMetadata, SimpleColumnMeta } from '../typings';
import { ExceptionConverter } from './ExceptionConverter';
import type { EntityManager } from '../EntityManager';
import type { Configuration } from '../utils/Configuration';
import type { IDatabaseDriver } from '../drivers/IDatabaseDriver';
import {
  ArrayType, BigIntType, BlobType, Uint8ArrayType, BooleanType, DateType, DecimalType, DoubleType, JsonType, SmallIntType, TimeType,
  TinyIntType, Type, UuidType, StringType, IntegerType, FloatType, DateTimeType, TextType, EnumType, UnknownType, MediumIntType, IntervalType,
} from '../types';
import { parseJsonSafe, Utils } from '../utils/Utils';
import { ReferenceKind } from '../enums';
import type { MikroORM } from '../MikroORM';
import type { TransformContext } from '../types/Type';

export const JsonProperty = Symbol('JsonProperty');

export abstract class Platform {

  protected readonly exceptionConverter = new ExceptionConverter();
  protected config!: Configuration;
  protected namingStrategy!: NamingStrategy;
  protected timezone?: string;

  usesPivotTable(): boolean {
    return false;
  }

  supportsTransactions(): boolean {
    return !this.config.get('disableTransactions');
  }

  usesImplicitTransactions(): boolean {
    return true;
  }

  getNamingStrategy(): { new(): NamingStrategy } {
    return UnderscoreNamingStrategy;
  }

  usesReturningStatement(): boolean {
    return false;
  }

  usesOutputStatement(): boolean {
    return false;
  }

  usesCascadeStatement(): boolean {
    return false;
  }

  /** for postgres native enums */
  supportsNativeEnums(): boolean {
    return false;
  }

  getSchemaHelper(): unknown {
    return undefined;
  }

  indexForeignKeys() {
    return false;
  }

  allowsMultiInsert() {
    return true;
  }

  /**
   * Whether or not the driver supports retuning list of created PKs back when multi-inserting
   */
  usesBatchInserts(): boolean {
    return true;
  }

  /**
   * Whether or not the driver supports updating many records at once
   */
  usesBatchUpdates(): boolean {
    return true;
  }

  usesDefaultKeyword(): boolean {
    return true;
  }

  /**
   * Normalizes primary key wrapper to scalar value (e.g. mongodb's ObjectId to string)
   */
  normalizePrimaryKey<T extends number | string = number | string>(data: Primary<T> | IPrimaryKey): T {
    return data as T;
  }

  /**
   * Converts scalar primary key representation to native driver wrapper (e.g. string to mongodb's ObjectId)
   */
  denormalizePrimaryKey(data: IPrimaryKey): IPrimaryKey {
    return data;
  }

  /**
   * Used when serializing via toObject and toJSON methods, allows to use different PK field name (like `id` instead of `_id`)
   */
  getSerializedPrimaryKeyField(field: string): string {
    return field;
  }

  usesDifferentSerializedPrimaryKey(): boolean {
    return false;
  }

  /**
   * Returns the SQL specific for the platform to get the current timestamp
   */
  getCurrentTimestampSQL(length?: number): string {
    return 'current_timestamp' + (length ? `(${length})` : '');
  }

  getDateTimeTypeDeclarationSQL(column: { length?: number }): string {
    return 'datetime' + (column.length ? `(${column.length})` : '');
  }

  getDefaultDateTimeLength(): number {
    return 0;
  }

  getDateTypeDeclarationSQL(length?: number): string {
    return 'date' + (length ? `(${length})` : '');
  }

  getTimeTypeDeclarationSQL(length?: number): string {
    return 'time' + (length ? `(${length})` : '');
  }

  getRegExpOperator(val?: unknown, flags?: string): string {
    return 'regexp';
  }

  getRegExpValue(val: RegExp): { $re: string; $flags?: string } {
    if (val.flags.includes('i')) {
      return { $re: `(?i)${val.source}` };
    }

    return { $re: val.source };
  }

  isAllowedTopLevelOperator(operator: string) {
    return operator === '$not';
  }

  quoteVersionValue(value: Date | number, prop: EntityProperty): Date | string | number {
    return value;
  }

  getDefaultVersionLength(): number {
    return 3;
  }

  allowsComparingTuples() {
    return true;
  }

  allowsUniqueBatchUpdates() {
    return true;
  }

  isBigIntProperty(prop: EntityProperty): boolean {
    return prop.columnTypes && prop.columnTypes[0] === 'bigint';
  }

  isRaw(value: any): boolean {
    return typeof value === 'object' && value !== null && '__raw' in value;
  }

  getDefaultSchemaName(): string | undefined {
    return undefined;
  }

  getBooleanTypeDeclarationSQL(): string {
    return 'boolean';
  }

  getIntegerTypeDeclarationSQL(column: { length?: number; unsigned?: boolean; autoincrement?: boolean }): string {
    return 'int';
  }

  getSmallIntTypeDeclarationSQL(column: { length?: number; unsigned?: boolean; autoincrement?: boolean }): string {
    return 'smallint';
  }

  getMediumIntTypeDeclarationSQL(column: { length?: number; unsigned?: boolean; autoincrement?: boolean }): string {
    return 'mediumint';
  }

  getTinyIntTypeDeclarationSQL(column: { length?: number; unsigned?: boolean; autoincrement?: boolean }): string {
    return 'tinyint';
  }

  getBigIntTypeDeclarationSQL(column: { length?: number; unsigned?: boolean; autoincrement?: boolean }): string {
    return 'bigint';
  }

  getVarcharTypeDeclarationSQL(column: { length?: number }): string {
    return `varchar(${column.length ?? 255})`;
  }

  getIntervalTypeDeclarationSQL(column: { length?: number }): string {
    return 'interval' + (column.length ? `(${column.length})` : '');
  }

  getTextTypeDeclarationSQL(_column: { length?: number }): string {
    return `text`;
  }

  getEnumTypeDeclarationSQL(column: { items?: unknown[]; fieldNames: string[]; length?: number; unsigned?: boolean; autoincrement?: boolean }): string {
    if (column.items?.every(item => Utils.isString(item))) {
      return `enum('${column.items.join("','")}')`;
    }

    return this.getTinyIntTypeDeclarationSQL(column);
  }

  getFloatDeclarationSQL(): string {
    return 'float';
  }

  getDoubleDeclarationSQL(): string {
    return 'double';
  }

  getDecimalTypeDeclarationSQL(column: { precision?: number; scale?: number }): string {
    const precision = column.precision ?? 10;
    const scale = column.scale ?? 0;

    return `numeric(${precision},${scale})`;
  }

  getUuidTypeDeclarationSQL(column: { length?: number }): string {
    column.length ??= 36;
    return this.getVarcharTypeDeclarationSQL(column);
  }

  extractSimpleType(type: string): string {
    return type.toLowerCase().match(/[^(), ]+/)![0];
  }

  getMappedType(type: string): Type<unknown> {
    const mappedType = this.config.get('discovery').getMappedType?.(type, this);
    return mappedType ?? this.getDefaultMappedType(type);
  }

  getDefaultMappedType(type: string): Type<unknown> {
    if (type.endsWith('[]')) {
      return Type.getType(ArrayType);
    }

    switch (this.extractSimpleType(type)) {
      case 'string':
      case 'varchar': return Type.getType(StringType);
      case 'interval': return Type.getType(IntervalType);
      case 'text': return Type.getType(TextType);
      case 'int':
      case 'number': return Type.getType(IntegerType);
      case 'bigint': return Type.getType(BigIntType);
      case 'smallint': return Type.getType(SmallIntType);
      case 'tinyint': return Type.getType(TinyIntType);
      case 'mediumint': return Type.getType(MediumIntType);
      case 'float': return Type.getType(FloatType);
      case 'double': return Type.getType(DoubleType);
      case 'integer': return Type.getType(IntegerType);
      case 'decimal':
      case 'numeric': return Type.getType(DecimalType);
      case 'boolean': return Type.getType(BooleanType);
      case 'blob':
      case 'buffer': return Type.getType(BlobType);
      case 'uint8array': return Type.getType(Uint8ArrayType);
      case 'uuid': return Type.getType(UuidType);
      case 'date': return Type.getType(DateType);
      case 'datetime':
      case 'timestamp': return Type.getType(DateTimeType);
      case 'time': return Type.getType(TimeType);
      case 'object':
      case 'json': return Type.getType(JsonType);
      case 'enum': return Type.getType(EnumType);
      default: return Type.getType(UnknownType);
    }
  }

  supportsMultipleCascadePaths(): boolean {
    return true;
  }

  supportsMultipleStatements(): boolean {
    return this.config.get('multipleStatements');
  }

  getArrayDeclarationSQL(): string {
    return 'text';
  }

  marshallArray(values: string[]): string {
    return values.join(',');
  }

  unmarshallArray(value: string): string[] {
    if (value === '') {
      return [];
    }

    return value.split(',') as string[];
  }

  getBlobDeclarationSQL(): string {
    return 'blob';
  }

  getJsonDeclarationSQL(): string {
    return 'json';
  }

  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string {
    return path;
  }

  getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string {
    return path.join('.');
  }

  /* istanbul ignore next */
  getJsonIndexDefinition(index: { columnNames: string[] }): string[] {
    return index.columnNames;
  }

  getFullTextWhereClause(prop: EntityProperty): string {
    throw new Error('Full text searching is not supported by this driver.');
  }

  supportsCreatingFullTextIndex(): boolean {
    return false;
  }

  getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string {
    throw new Error('Full text searching is not supported by this driver.');
  }

  convertsJsonAutomatically(): boolean {
    return true;
  }

  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown {
    return JSON.stringify(value);
  }

  convertJsonToJSValue(value: unknown): unknown {
    return parseJsonSafe(value);
  }

  convertDateToJSValue(value: string | Date): string {
    return value as string;
  }

  convertIntervalToJSValue(value: string): unknown {
    return value;
  }

  convertIntervalToDatabaseValue(value: unknown): unknown {
    return value;
  }

  parseDate(value: string | number): Date {
    const date = new Date(value);

    if (isNaN(date.getTime())) {
      return value as unknown as Date;
    }

    return date;
  }

  getRepositoryClass<T extends object>(): Constructor<EntityRepository<T>> {
    return EntityRepository;
  }

  getDefaultCharset(): string {
    return 'utf8';
  }

  getExceptionConverter(): ExceptionConverter {
    return this.exceptionConverter;
  }

  /**
   * Allows registering extensions of the driver automatically (e.g. `SchemaGenerator` extension in SQL drivers).
   */
  lookupExtensions(orm: MikroORM): void {
    // no extensions by default
  }

  getExtension<T>(extensionName: string, extensionKey: string, moduleName: string, em: EntityManager): T {
    const extension = this.config.getExtension<T>(extensionKey);

    if (extension) {
      return extension;
    }

    /* istanbul ignore next */
    const module = Utils.tryRequire({
      module: moduleName,
      warning: `Please install ${moduleName} package.`,
    });

    /* istanbul ignore next */
    if (module) {
      return this.config.getCachedService(module[extensionName], em);
    }

    /* istanbul ignore next */
    throw new Error(`${extensionName} extension not registered.`);
  }

  /* istanbul ignore next: kept for type inference only */
  getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): ISchemaGenerator {
    throw new Error(`${driver.constructor.name} does not support SchemaGenerator`);
  }

  processDateProperty(value: unknown): string | number | Date {
    return value as string;
  }

  quoteIdentifier(id: string, quote = '`'): string {
    return `${quote}${id.toString().replace('.', `${quote}.${quote}`)}${quote}`;
  }

  quoteValue(value: any): string {
    return value;
  }

  formatQuery(sql: string, params: readonly any[]): string {
    if (params.length === 0) {
      return sql;
    }

    // fast string replace without regexps
    let j = 0;
    let pos = 0;
    let ret = '';

    if (sql[0] === '?') {
      if (sql[1] === '?') {
        ret += this.quoteIdentifier(params[j++]);
        pos = 2;
      } else {
        ret += this.quoteValue(params[j++]);
        pos = 1;
      }
    }

    while (pos < sql.length) {
      const idx = sql.indexOf('?', pos + 1);

      if (idx === -1) {
        ret += sql.substring(pos, sql.length);
        break;
      }

      if (sql.substring(idx - 1, idx + 1) === '\\?') {
        ret += sql.substring(pos, idx - 1) + '?';
        pos = idx + 1;
      } else if (sql.substring(idx, idx + 2) === '??') {
        ret += sql.substring(pos, idx) + this.quoteIdentifier(params[j++]);
        pos = idx + 2;
      } else {
        ret += sql.substring(pos, idx) + this.quoteValue(params[j++]);
        pos = idx + 1;
      }
    }

    return ret;
  }

  cloneEmbeddable<T>(data: T): T {
    const copy = clone(data);
    // tag the copy so we know it should be stringified when quoting (so we know how to treat JSON arrays)
    Object.defineProperty(copy, JsonProperty, { enumerable: false, value: true });

    return copy;
  }

  setConfig(config: Configuration): void {
    this.config = config;
    this.namingStrategy = config.getNamingStrategy();

    if (this.config.get('forceUtcTimezone')) {
      this.timezone = 'Z';
    } else {
      this.timezone = this.config.get('timezone');
    }
  }

  getConfig(): Configuration {
    return this.config;
  }

  getTimezone() {
    return this.timezone;
  }

  isNumericColumn(mappedType: Type<unknown>): boolean {
    return [IntegerType, SmallIntType, BigIntType, TinyIntType].some(t => mappedType instanceof t);
  }

  supportsUnsigned(): boolean {
    return false;
  }

  /**
   * Returns the default name of index for the given columns
   */
  getIndexName(tableName: string, columns: string[], type: 'index' | 'unique' | 'foreign' | 'primary' | 'sequence'): string {
    return this.namingStrategy.indexName(tableName, columns, type);
  }

  /* istanbul ignore next */
  getDefaultPrimaryName(tableName: string, columns: string[]): string {
    return this.namingStrategy.indexName(tableName, columns, 'primary');
  }

  supportsCustomPrimaryKeyNames(): boolean {
    return false;
  }

  isPopulated<T>(key: string, populate: PopulateOptions<T>[] | boolean): boolean {
    return populate === true || (populate !== false && populate.some(p => p.field === key || p.all));
  }

  shouldHaveColumn<T>(prop: EntityProperty<T>, populate: PopulateOptions<T>[] | boolean, exclude?: string[], includeFormulas = true): boolean {
    if (exclude?.includes(prop.name)) {
      return false;
    }

    if (exclude?.find(k => k.startsWith(`${prop.name}.`) && !this.isPopulated(prop.name, populate))) {
      return false;
    }

    if (prop.formula) {
      return includeFormulas && (!prop.lazy || this.isPopulated(prop.name, populate));
    }

    if (prop.persist === false) {
      return false;
    }

    if (prop.lazy && (populate === false || (populate !== true && !populate.some(p => p.field === prop.name)))) {
      return false;
    }

    if ([ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
      return true;
    }

    if (prop.kind === ReferenceKind.EMBEDDED) {
      return !!prop.object;
    }

    return prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner;
  }

  /**
   * Currently not supported due to how knex does complex sqlite diffing (always based on current schema)
   */
  supportsDownMigrations(): boolean {
    return true;
  }

  validateMetadata(meta: EntityMetadata): void {
    return;
  }

  /**
   * Generates a custom order by statement given a set of in order values, eg.
   * ORDER BY (CASE WHEN priority = 'low' THEN 1 WHEN priority = 'medium' THEN 2 ELSE NULL END)
   */
  generateCustomOrder(escapedColumn: string, values: unknown[]) {
    throw new Error('Not supported');
  }

  /**
   * @internal
   */
  castColumn(prop?: { columnTypes?: string[] }): string {
    return '';
  }

  /**
   * @internal
   */
  castJsonValue(prop?: { columnTypes?: string[] }): string {
    return '';
  }

  /**
   * @internal
   */
  clone() {
    return this;
  }

}