repo_description: A fast, local first, reactive Database for JavaScript Applications https://rxdb.info/
repo_topics: angular browser-database couchdb crdt database firebase graphql indexeddb ionic local-first localstorage nodejs nosql postgres pouchdb react-native realtime realtime-database rxdb sqlite
code: import { newRxError } from './rx-error.ts';
  import type {
      CompositePrimaryKey,
      DeepReadonly,
      JsonSchema,
      PrimaryKey,
      RxDocumentData,
      RxJsonSchema,
      RxStorageDefaultCheckpoint,
      StringKeys
  } from './types/index.d.ts';
  import {
      ensureNotFalsy,
      flatClone,
      getProperty,
      isMaybeReadonlyArray,
      REGEX_ALL_DOTS,
      RX_META_LWT_MINIMUM,
      sortObject,
      trimDots
  } from './plugins/utils/index.ts';
  import type { RxSchema } from './rx-schema.ts';
  
  /**
   * Helper function to create a valid RxJsonSchema
   * with a given version.
   */
  export function getPseudoSchemaForVersion<T = any>(
      version: number,
      primaryKey: StringKeys<T>
  ): RxJsonSchema<RxDocumentData<T>> {
      const pseudoSchema: RxJsonSchema<RxDocumentData<T>> = fillWithDefaultSettings({
          version,
          type: 'object',
          primaryKey: primaryKey as any,
          properties: {
              [primaryKey]: {
                  type: 'string',
                  maxLength: 100
              },
              value: {
                  type: 'string'
              }
          } as any,
          indexes: [
              [primaryKey]
          ],
          required: [primaryKey]
      });
      return pseudoSchema;
  }
  
  /**
   * Cache for getSchemaByObjectPath results.
   * Uses a WeakMap keyed by the schema object reference
   * so the cache is automatically cleaned up when schemas are garbage collected.
   */
  const SCHEMA_PATH_CACHE = new WeakMap<object, Map<string, JsonSchema>>();
  
  /**
   * Returns the sub-schema for a given path.
   * Results are cached per schema reference and path
   * to avoid redundant string operations and property lookups.
   */
  export function getSchemaByObjectPath<T = any>(
      rxJsonSchema: RxJsonSchema<T>,
      path: keyof T | string
  ): JsonSchema {
      let pathCache = SCHEMA_PATH_CACHE.get(rxJsonSchema as any);
      const pathStr = path as string;
      if (pathCache) {
          const cached = pathCache.get(pathStr);
          if (cached) {
              return cached;
          }
      } else {
          pathCache = new Map();
          SCHEMA_PATH_CACHE.set(rxJsonSchema as any, pathCache);
      }
  
      let usePath: string = pathStr;
      usePath = usePath.replace(REGEX_ALL_DOTS, '.properties.');
      usePath = 'properties.' + usePath;
      usePath = trimDots(usePath);
  
      const ret = getProperty(rxJsonSchema, usePath);
      pathCache.set(pathStr, ret);
      return ret;
  }
  
  export function fillPrimaryKey<T>(
      primaryPath: keyof T,
      jsonSchema: RxJsonSchema<T>,
      documentData: RxDocumentData<T>
  ): RxDocumentData<T> {
      // optimization shortcut.
      if (typeof jsonSchema.primaryKey === 'string') {
          return documentData;
      }
  
      const newPrimary = getComposedPrimaryKeyOfDocumentData<T>(
          jsonSchema,
          documentData
      );
      const existingPrimary: string | undefined = documentData[primaryPath] as any;
      if (
          existingPrimary &&
          existingPrimary !== newPrimary
      ) {
          throw newRxError(
              'DOC19',
              {
                  args: {
                      documentData,
                      existingPrimary,
                      newPrimary,
                  },
                  schema: jsonSchema
              });
      }
  
      (documentData as any)[primaryPath] = newPrimary;
      return documentData;
  }
  
  export function getPrimaryFieldOfPrimaryKey<RxDocType>(
      primaryKey: PrimaryKey<RxDocType>
  ): StringKeys<RxDocType> {
      if (typeof primaryKey === 'string') {
          return primaryKey as any;
      } else {
          return (primaryKey as CompositePrimaryKey<RxDocType>).key;
      }
  }
  
  export function getLengthOfPrimaryKey<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>
  ): number {
      const primaryPath = getPrimaryFieldOfPrimaryKey(schema.primaryKey);
      const schemaPart = getSchemaByObjectPath(schema, primaryPath);
      return ensureNotFalsy(schemaPart.maxLength);
  }
  
  /**
   * Returns the composed primaryKey of a document by its data.
   */
  export function getComposedPrimaryKeyOfDocumentData<RxDocType>(
      jsonSchema: RxJsonSchema<RxDocType> | RxJsonSchema<RxDocumentData<RxDocType>>,
      documentData: Partial<RxDocType>
  ): string {
      if (typeof jsonSchema.primaryKey === 'string') {
          return (documentData as any)[jsonSchema.primaryKey];
      }
  
      const compositePrimary: CompositePrimaryKey<RxDocType> = jsonSchema.primaryKey as any;
      return compositePrimary.fields.map(field => {
          const value = getProperty(documentData as any, field as string);
          if (typeof value === 'undefined') {
              throw newRxError('DOC18', { args: { field, documentData } });
          }
          return value;
      }).join(compositePrimary.separator);
  }
  
  
  /**
   * Normalize the RxJsonSchema.
   * We need this to ensure everything is set up properly
   * and we have the same hash on schemas that represent the same value but
   * have different json.
   *
   * - Orders the schemas attributes by alphabetical order
   * - Adds the primaryKey to all indexes that do not contain the primaryKey
   * - We need this for deterministic sort order on all queries, which is required for event-reduce to work.
   *
   * @return RxJsonSchema - ordered and filled
   */
  export function normalizeRxJsonSchema<T>(jsonSchema: RxJsonSchema<T>): RxJsonSchema<T> {
      const normalizedSchema: RxJsonSchema<T> = sortObject(jsonSchema, true);
      return normalizedSchema;
  }
  
  /**
   * If the schema does not specify any index,
   * we add this index so we at least can run RxQuery()
   * and only select non-deleted fields.
   */
  export function getDefaultIndex(primaryPath: string) {
      return ['_deleted', primaryPath];
  }
  
  /**
   * fills the schema-json with default-settings
   * @return cloned schemaObj
   */
  export function fillWithDefaultSettings<T = any>(
      schemaObj: RxJsonSchema<T>
  ): RxJsonSchema<RxDocumentData<T>> {
      schemaObj = flatClone(schemaObj);
      const primaryPath: string = getPrimaryFieldOfPrimaryKey(schemaObj.primaryKey);
      schemaObj.properties = flatClone(schemaObj.properties);
  
      // additionalProperties is always false
      schemaObj.additionalProperties = false;
  
      // fill with key-compression-state ()
      if (!Object.prototype.hasOwnProperty.call(schemaObj, 'keyCompression')) {
          schemaObj.keyCompression = false;
      }
  
      // indexes must be array
      schemaObj.indexes = schemaObj.indexes ? schemaObj.indexes.slice(0) : [];
  
      // required must be array
      schemaObj.required = schemaObj.required ? schemaObj.required.slice(0) : [];
  
      // encrypted must be array
      schemaObj.encrypted = schemaObj.encrypted ? schemaObj.encrypted.slice(0) : [];
  
      // add _rev
      (schemaObj.properties as any)._rev = {
          type: 'string',
          minLength: 1
      };
  
      // add attachments
      (schemaObj.properties as any)._attachments = {
          type: 'object'
      };
  
      // add deleted flag
      (schemaObj.properties as any)._deleted = {
          type: 'boolean'
      };
  
      // add meta property
      (schemaObj.properties as any)._meta = RX_META_SCHEMA;
  
      /**
       * meta fields are all required
       */
      schemaObj.required = schemaObj.required ? schemaObj.required.slice(0) : [];
      (schemaObj.required as string[]).push('_deleted');
      (schemaObj.required as string[]).push('_rev');
      (schemaObj.required as string[]).push('_meta');
      (schemaObj.required as string[]).push('_attachments');
  
      // primaryKey is always required
      (schemaObj.required as any).push(primaryPath);
  
      schemaObj.required = schemaObj.required
          .filter((field: string) => !field.includes('.'))
          .filter((elem: any, pos: any, arr: any) => arr.indexOf(elem) === pos); // unique;
  
  
      // version is 0 by default
      schemaObj.version = schemaObj.version || 0;
  
      const useIndexes: string[][] = schemaObj.indexes.map(index => {
          const arIndex = isMaybeReadonlyArray(index) ? index.slice(0) : [index];
          /**
           * Append primary key to indexes that do not contain the primaryKey.
           * All indexes must have the primaryKey to ensure a deterministic sort order.
           */
          if (!arIndex.includes(primaryPath)) {
              arIndex.push(primaryPath);
          }
  
          // add _deleted flag to all indexes so we can query only non-deleted fields
          // in RxDB itself
          if (arIndex[0] !== '_deleted') {
              arIndex.unshift('_deleted');
          }
  
          return arIndex;
      });
  
      if (useIndexes.length === 0) {
          useIndexes.push(getDefaultIndex(primaryPath));
      }
  
      // we need this index for the getChangedDocumentsSince() method
      useIndexes.push(['_meta.lwt', primaryPath]);
  
      // also add the internalIndexes
      if (schemaObj.internalIndexes) {
          schemaObj.internalIndexes.map(idx => {
              useIndexes.push(idx);
          });
      }
  
      // make indexes unique
      const hasIndex = new Set<string>();
      schemaObj.indexes = useIndexes.filter(index => {
          const indexStr = index.join(',');
          if (hasIndex.has(indexStr)) {
              return false;
          } else {
              hasIndex.add(indexStr);
              return true;
          }
      });
  
      return schemaObj as any;
  }
  
  export const META_LWT_UNIX_TIME_MAX = 1000000000000000;
  export const RX_META_SCHEMA: JsonSchema = {
      type: 'object',
      properties: {
          /**
           * The last-write time.
           * Unix time in milliseconds.
           */
          lwt: {
              type: 'number',
              /**
               * We use 1 as minimum so that the value is never falsy.
               */
              minimum: RX_META_LWT_MINIMUM,
              maximum: META_LWT_UNIX_TIME_MAX,
              multipleOf: 0.01
          }
      },
      /**
       * Additional properties are allowed
       * and can be used by plugins to set various flags.
       */
      additionalProperties: true as any,
      required: [
          'lwt'
      ]
  };
  
  
  /**
   * returns the final-fields of the schema
   * @return field-names of the final-fields
   */
  export function getFinalFields<T = any>(
      jsonSchema: RxJsonSchema<T>
  ): string[] {
      const ret = Object.keys(jsonSchema.properties)
          .filter(key => (jsonSchema as any).properties[key].final);
  
      // primary is also final
      const primaryPath = getPrimaryFieldOfPrimaryKey(jsonSchema.primaryKey);
      ret.push(primaryPath);
  
      // fields of composite primary are final
      if (typeof jsonSchema.primaryKey !== 'string') {
          (jsonSchema.primaryKey as CompositePrimaryKey<T>).fields
              .forEach(field => ret.push(field as string));
      }
  
      return ret;
  }
  
  /**
   * fills all unset fields with default-values if set
   * @hotPath
   */
  export function fillObjectWithDefaults(rxSchema: RxSchema<any>, obj: any): any {
      const defaultKeys = Object.keys(rxSchema.defaultValues);
      for (let i = 0; i < defaultKeys.length; ++i) {
          const key = defaultKeys[i];
          if (obj[key] === undefined) {
              const val = rxSchema.defaultValues[key];
              if (typeof val === 'object' && val !== null) {
                  obj[key] = Array.isArray(val) ? val.slice() : { ...val };
              } else {
                  obj[key] = val;
              }
          }
      }
      return obj;
  }
  
  export const DEFAULT_CHECKPOINT_SCHEMA: DeepReadonly<JsonSchema<RxStorageDefaultCheckpoint>> = {
      type: 'object',
      properties: {
          id: {
              type: 'string'
          },
          lwt: {
              type: 'number'
          }
      },
      required: [
          'id',
          'lwt'
      ],
      additionalProperties: false
  } as const;
  