util.ts•3.61 kB
import { z } from 'zod';
import type { SupabasePlatform } from './platform/types.js';
import { PLATFORM_INDEPENDENT_FEATURES } from './server.js';
import {
  currentFeatureGroupSchema,
  featureGroupSchema,
  type FeatureGroup,
} from './types.js';
export type ValueOf<T> = T[keyof T];
// UnionToIntersection<A | B> = A & B
export type UnionToIntersection<U> = (
  U extends unknown
    ? (arg: U) => 0
    : never
) extends (arg: infer I) => 0
  ? I
  : never;
// LastInUnion<A | B> = B
export type LastInUnion<U> = UnionToIntersection<
  U extends unknown ? (x: U) => 0 : never
> extends (x: infer L) => 0
  ? L
  : never;
// UnionToTuple<A, B> = [A, B]
export type UnionToTuple<T, Last = LastInUnion<T>> = [T] extends [never]
  ? []
  : [Last, ...UnionToTuple<Exclude<T, Last>>];
/**
 * Parses a key-value string into an object.
 *
 * @returns An object representing the key-value pairs
 *
 * @example
 * const result = parseKeyValueList("key1=value1\nkey2=value2");
 * console.log(result); // { key1: "value1", key2: "value2" }
 */
export function parseKeyValueList(data: string): { [key: string]: string } {
  return Object.fromEntries(
    data
      .split('\n')
      .map((item) => item.split(/=(.*)/)) // split only on the first '='
      .filter(([key]) => key) // filter out empty keys
      .map(([key, value]) => [key, value ?? '']) // ensure value is not undefined
  );
}
/**
 * Creates a unique hash from a JavaScript object.
 * @param obj - The object to hash
 * @param length - Optional length to truncate the hash (default: full length)
 */
export async function hashObject(
  obj: Record<string, any>,
  length?: number
): Promise<string> {
  // Sort object keys to ensure consistent output regardless of original key order
  const str = JSON.stringify(obj, (_, value) => {
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      return Object.keys(value)
        .sort()
        .reduce<Record<string, any>>((result, key) => {
          result[key] = value[key];
          return result;
        }, {});
    }
    return value;
  });
  const buffer = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(str)
  );
  // Convert to base64
  const base64Hash = btoa(String.fromCharCode(...new Uint8Array(buffer)));
  return base64Hash.slice(0, length);
}
/**
 * Parses and validates feature groups based on the platform's available features.
 */
export function parseFeatureGroups(
  platform: SupabasePlatform,
  features: string[]
) {
  // First pass: validate that all features are valid
  const desiredFeatures = z.set(featureGroupSchema).parse(new Set(features));
  // The platform implementation can define a subset of features
  const availableFeatures: FeatureGroup[] = [
    ...PLATFORM_INDEPENDENT_FEATURES,
    ...currentFeatureGroupSchema.options.filter((key) =>
      Object.keys(platform).includes(key)
    ),
  ];
  const availableFeaturesSchema = z.enum(
    availableFeatures as [string, ...string[]],
    {
      description: 'Available features based on platform implementation',
      errorMap: (issue, ctx) => {
        switch (issue.code) {
          case 'invalid_enum_value':
            return {
              message: `This platform does not support the '${issue.received}' feature group. Supported groups are: ${availableFeatures.join(', ')}`,
            };
          default:
            return { message: ctx.defaultError };
        }
      },
    }
  );
  // Second pass: validate the desired features against this platform's available features
  return z.set(availableFeaturesSchema).parse(desiredFeatures);
}