Source

indexes/generator.ts

import { IndexMetadata, OrderDirection, PersistenceKeys } from "@decaf-ts/core";
import { CouchDBKeys } from "../constants";
import { DefaultSeparator } from "@decaf-ts/db-decorators";
import { Model } from "@decaf-ts/decorator-validation";
import { CouchDBOperator } from "../query/constants";
import { CreateIndexRequest } from "../types";
import { Constructor } from "@decaf-ts/decoration";
import { generateViewIndexes } from "../views";

/**
 * @description Generates a name for a CouchDB index
 * @summary Creates a standardized name for a CouchDB index by combining name parts, compositions, and direction
 * @param {string[]} name - Array of name parts for the index
 * @param {OrderDirection} [direction] - Optional sort direction for the index
 * @param {string[]} [compositions] - Optional additional attributes to include in the index name
 * @param {string} [separator=DefaultSeparator] - The separator to use between parts of the index name
 * @return {string} The generated index name
 * @memberOf module:for-couchdb
 */
function generateIndexName(
  name: string[],
  direction?: OrderDirection,
  compositions?: string[],
  separator = DefaultSeparator
) {
  return [
    ...name.map((n) => (n === CouchDBKeys.TABLE ? "table" : n)),
    ...(compositions || []),
    ...(direction ? [direction] : []),
    CouchDBKeys.INDEX,
  ].join(separator);
}

/**
 * @description Generates CouchDB index configurations for models
 * @summary Creates a set of CouchDB index configurations based on the metadata of the provided models
 * @template M - The model type that extends Model
 * @param models - Array of model constructors to generate indexes for
 * @return {CreateIndexRequest[]} Array of CouchDB index configurations
 * @function generateIndexes
 * @memberOf module:for-couchdb
 * @mermaid
 * sequenceDiagram
 *   participant Caller
 *   participant generateIndexes
 *   participant generateIndexName
 *   participant Repository
 *
 *   Caller->>generateIndexes: models
 *
 *   Note over generateIndexes: Create base table index
 *   generateIndexes->>generateIndexName: [CouchDBKeys.TABLE]
 *   generateIndexName-->>generateIndexes: tableName
 *   generateIndexes->>generateIndexes: Create table index config
 *
 *   loop For each model
 *     generateIndexes->>Repository: Get indexes metadata
 *     Repository-->>generateIndexes: index metadata
 *
 *     loop For each index in metadata
 *       Note over generateIndexes: Extract index properties
 *       generateIndexes->>Repository: Get table name
 *       Repository-->>generateIndexes: tableName
 *
 *       Note over generateIndexes: Define nested generate function
 *
 *       generateIndexes->>generateIndexes: Call generate() for default order
 *       Note over generateIndexes: Create index name and config
 *
 *       alt Has directions
 *         loop For each direction
 *           generateIndexes->>generateIndexes: Call generate(direction)
 *           Note over generateIndexes: Create ordered index config
 *         end
 *       end
 *     end
 *   end
 *
 *   generateIndexes-->>Caller: Array of index configurations
 */
export function generateIndexes<M extends Model>(
  models: Constructor<M>[]
): CreateIndexRequest[] {
  const tableName = generateIndexName([CouchDBKeys.TABLE]);
  const indexes: Record<string, CreateIndexRequest> = {};
  indexes[tableName] = {
    index: {
      fields: [CouchDBKeys.TABLE],
    },
    name: tableName,
    ddoc: tableName,
    type: "json",
  };

  models.forEach((m) => {
    const modelTableName = Model.tableName(m);
    let defaultQueryAttrs: string[] = [];
    try {
      defaultQueryAttrs = Model.defaultQueryAttributes(m);
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
    } catch (err) {
      defaultQueryAttrs = [];
    }
    defaultQueryAttrs.forEach((attr) => {
      const baseName = generateIndexName([
        modelTableName,
        attr,
        "defaultQuery",
      ]);
      const defaultFilter: Record<string, any> = {
        [CouchDBKeys.TABLE]: {
          [CouchDBOperator.EQUAL]: modelTableName,
        },
      };
      indexes[baseName] = {
        index: {
          fields: [CouchDBKeys.TABLE, attr],
        },
        name: baseName,
        ddoc: baseName,
        type: "json",
      };
      indexes[baseName].index.partial_filter_selector = defaultFilter;

      [OrderDirection.ASC, OrderDirection.DSC].forEach((direction) => {
        const sortedName = generateIndexName(
          [modelTableName, attr, "defaultQuery"],
          direction
        );
        indexes[sortedName] = {
          index: {
            fields: [
              {
                [CouchDBKeys.TABLE]: direction,
              },
              {
                [attr]: direction,
              },
            ],
          },
          name: sortedName,
          ddoc: sortedName,
          type: "json",
        };
      });
    });

    const ind: Record<string, IndexMetadata> = Model.indexes(m);
    Object.entries(ind).forEach(([key, value]) => {
      const metadataEntries: [string, IndexMetadata][] = [];
      Object.entries(value || {}).forEach(([, metadataValue]) => {
        if (!metadataValue) return;
        const candidate = metadataValue as IndexMetadata;
        if (
          candidate.directions !== undefined ||
          candidate.compositions !== undefined
        ) {
          metadataEntries.push([key, candidate]);
          return;
        }
        if (
          typeof metadataValue === "object" &&
          !Array.isArray(metadataValue)
        ) {
          const nested = metadataValue as Record<string, IndexMetadata>;
          Object.entries(nested).forEach(([field, meta]) => {
            if (meta) metadataEntries.push([field, meta]);
          });
        }
      });

      metadataEntries.forEach(([fieldKey, meta]) => {
        if (!meta) return;
        // eslint-disable-next-line prefer-const
        let { directions, compositions } = meta as any;
        const tableName = modelTableName;
        compositions = compositions || [];
        const fieldKeys = [fieldKey, ...(compositions as string[])];

        const tableFilter: Record<string, any> = {
          [CouchDBKeys.TABLE]: {
            [CouchDBOperator.EQUAL]: tableName,
          },
        };

        function generate(sort?: OrderDirection, suffix?: string) {
          const name = [
            tableName,
            fieldKey,
            ...(compositions as []),
            ...(suffix ? [suffix] : []),
            PersistenceKeys.INDEX,
          ].join(DefaultSeparator);

          const baseFields = [CouchDBKeys.TABLE, ...fieldKeys];
          const fields = sort
            ? [
                {
                  [CouchDBKeys.TABLE]: sort,
                },
                ...fieldKeys.map((sortField) => ({
                  [sortField]: sort,
                })),
              ]
            : baseFields;

          indexes[name] = {
            index: {
              fields,
            },
            name,
            ddoc: name,
            type: "json",
          };
          if (!sort) {
            indexes[name].index.partial_filter_selector = tableFilter;
          }
        }

        generate();
        const normalizedDirections = Array.from(
          new Set(
            (directions || [OrderDirection.ASC]).map(
              (dir: OrderDirection | string) => String(dir).toLowerCase()
            )
          )
        );

        const validDirections = normalizedDirections.filter(
          (dir): dir is OrderDirection =>
            dir === OrderDirection.ASC || dir === OrderDirection.DSC
        );

        validDirections.forEach((direction) => {
          generate(direction, direction);
        });
      });
    });
  });

  generateViewIndexes(models).forEach((index) => {
    if (!index.name) return;
    indexes[index.name] = index;
  });

  return Object.values(indexes);
}