Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

supabase gen types ignores NOT NULL

Open
#6,762 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
65/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
postgresql, supabase, typescript
Domain
cli, databases, tooling

Research direction

The issue is in the Supabase CLI's type generation for views. Start by examining the code that generates TypeScript types from the database schema, likely in the gen command area. Look for where view column nullability is determined. The user's script shows they parse view definitions with @supabase/pg-parser; you'll need to integrate similar logic to respect NOT NULL constraints from source tables. Test by creating a view with a NOT NULL column and running supabase gen types typescript to see the output.

Written by the indexing model from the issue text.

Description

🐛 Bug supabase/cli
Affected area

Database

Supabase CLI version

v2.117.0

Operating system

macOS

Installation method

brew

Command
supabase gen types typescript
Actual output
All `NOT NULL` columns in views are defined as `string | null`, `boolean | null`.
Expected behavior

NOT NULL columns in views should be defined as string, boolean etc.

Steps to reproduce
  1. Create a view in database.
  2. Execute supabase gen types typescript --local > src/shared/database.types.ts
Crash report ID

No response

Docker and service versions

Additional context

I created a workaround script that reads the generated file and modifies it to overwrite columns to make them NOT NULL.

Example Usage
node --env-file=.env.test.local scripts/generate-supabase-view-types-with-not-nulls.mjs --schema api,service_api --input src/shared/database.types.ts

generate-supabase-view-types-with-not-nulls.mjs

#!/usr/bin/env node

import { readFile, writeFile } from "node:fs/promises";
import process from "node:process";

import { getSupportedVersions, PgParser, unwrapNode, unwrapParseResult } from "@supabase/pg-parser";
import pg from "pg";

const { Client } = pg;

const enrichmentBanner = `// =============================================================================
// Supabase view NOT NULL type enrichment
// =============================================================================
// Everything above this banner is generated by the Supabase CLI.
// Everything below it is appended by
// scripts/generate-supabase-view-types-with-not-nulls.mjs and may be regenerated.`;

/**
 * Generates a corrected Supabase Database type for PostgreSQL views.
 *
 * PostgreSQL does not reliably expose view-column nullability through
 * attnotnull, so Supabase may generate:
 *
 *   id: number | null
 *
 * even when the view directly selects a NOT NULL source column.
 *
 * This script:
 *
 *   1. Reads view definitions from PostgreSQL.
 *   2. Parses them using PostgreSQL's actual parser via @supabase/pg-parser.
 *   3. Proves which view columns are definitely NOT NULL.
 *   4. Makes the generated Database type internal and appends an enriched
 *      Database export to the input file.
 *   5. Points generated helper types at the enriched Database type.
 *
 * Supported:
 *
 *   SELECT t.id
 *   SELECT t.id AS another_name
 *   INNER JOIN
 *   LEFT JOIN
 *   RIGHT JOIN
 *   FULL JOIN
 *   nested views
 *   table NOT NULL constraints
 *   primary-key NOT NULL constraints
 *   NOT NULL domains
 *   multiple schemas
 *
 * Examples:
 *
 *   --schema api
 *   --schema api,service_api
 *
 * Deliberately NOT inferred:
 *
 *   functions
 *   casts
 *   CASE
 *   COALESCE
 *   arithmetic
 *   expressions
 *   subqueries in FROM
 *   UNION / INTERSECT / EXCEPT
 *   WHERE x IS NOT NULL
 *   nullability implied by JOIN predicates
 *
 * False negatives are acceptable.
 * False positives are not.
 */

const options = parseArgs(process.argv.slice(2));

const databaseUrl = options.dbUrl ?? process.env.DATABASE_URL ?? process.env.SUPABASE_DB_URL;

if (!databaseUrl) {
  throw new Error(
    "Database URL is missing. " + "Set DATABASE_URL or SUPABASE_DB_URL, or pass --db-url.",
  );
}

const client = new Client({
  connectionString: databaseUrl,
});

await client.connect();

try {
  await client.query("BEGIN");

  /**
   * Make pg_get_viewdef() schema-qualify application relations where
   * possible. This makes relation resolution substantially safer.
   */
  await client.query("SET LOCAL search_path TO pg_catalog");

  const databaseInfo = await getDatabaseInfo();

  const postgresMajor = await getPostgresMajorVersion();

  const supportedVersions = getSupportedVersions();

  if (!supportedVersions.includes(postgresMajor)) {
    throw new Error(
      `PostgreSQL ${postgresMajor} is not supported by ` +
        `@supabase/pg-parser.\n` +
        `Supported versions: ${supportedVersions.join(", ")}`,
    );
  }

  const parser = new PgParser({
    version: postgresMajor,
  });

  /**
   * schema.relation -> relation metadata
   */
  const relationCache = new Map();

  /**
   * schema.view -> Set<columnName>
   */
  const viewInferenceCache = new Map();

  /**
   * ---------------------------------------------------------
   * Relation metadata
   * ---------------------------------------------------------
   */

  async function getRelation(schemaName, relationName) {
    const key = relationKey(schemaName, relationName);

    if (relationCache.has(key)) {
      return relationCache.get(key);
    }

    const relationResult = await client.query(
      `
          SELECT
            c.oid::text AS oid,
            n.nspname AS schema_name,
            c.relname AS relation_name,
            c.relkind,

            CASE
              WHEN c.relkind IN ('v', 'm')
              THEN pg_catalog.pg_get_viewdef(
                c.oid,
                false
              )
              ELSE NULL
            END AS definition

          FROM pg_catalog.pg_class AS c

          JOIN pg_catalog.pg_namespace AS n
            ON n.oid = c.relnamespace

          WHERE n.nspname = $1
            AND c.relname = $2

          LIMIT 1
        `,
      [schemaName, relationName],
    );

    if (relationResult.rowCount === 0) {
      relationCache.set(key, null);

      return null;
    }

    const relationRow = relationResult.rows[0];

    const columnResult = await client.query(
      `
          SELECT
            a.attnum,
            a.attname,

            (
              a.attnotnull
              OR t.typnotnull
            ) AS not_null

          FROM pg_catalog.pg_attribute AS a

          JOIN pg_catalog.pg_type AS t
            ON t.oid = a.atttypid

          WHERE a.attrelid = $1::oid
            AND a.attnum > 0
            AND NOT a.attisdropped

          ORDER BY a.attnum
        `,
      [relationRow.oid],
    );

    const orderedColumns = columnResult.rows.map((row) => ({
      name: row.attname,
      notNull: row.not_null === true,
    }));

    const columns = new Map(orderedColumns.map((column) => [column.name, column]));

    const relation = {
      oid: relationRow.oid,

      schema: relationRow.schema_name,

      name: relationRow.relation_name,

      relkind: relationRow.relkind,

      definition: relationRow.definition,

      columns,
      orderedColumns,
    };

    relationCache.set(key, relation);

    return relation;
  }

  async function relationHasColumn(source, columnName) {
    if (!source.schema) {
      return false;
    }

    const relation = await getRelation(source.schema, source.relation);

    return relation?.columns.has(columnName) ?? false;
  }

  /**
   * ---------------------------------------------------------
   * Column nullability
   * ---------------------------------------------------------
   */

  async function isRelationColumnNotNull(schemaName, relationName, columnName, visiting) {
    const relation = await getRelation(schemaName, relationName);

    if (!relation) {
      return false;
    }

    const column = relation.columns.get(columnName);

    if (!column) {
      return false;
    }

    /**
     * Real PostgreSQL NOT NULL metadata.
     *
     * This includes:
     *
     * - explicit NOT NULL
     * - primary-key NOT NULL
     * - NOT NULL domains
     */
    if (column.notNull) {
      return true;
    }

    /**
     * Views generally lose NOT NULL metadata, so recursively
     * inspect the view definition.
     */
    if (relation.relkind === "v" || relation.relkind === "m") {
      const inferred = await inferViewColumns(schemaName, relationName, visiting);

      return inferred.has(columnName);
    }

    return false;
  }

  /**
   * ---------------------------------------------------------
   * View inference
   * ---------------------------------------------------------
   */

  async function inferViewColumns(schemaName, viewName, visiting = new Set()) {
    const key = relationKey(schemaName, viewName);

    const cached = viewInferenceCache.get(key);

    if (cached) {
      return cached;
    }

    /**
     * Protect against recursive views.
     */
    if (visiting.has(key)) {
      verbose(`Skipping recursive reference: ${key}`);

      return new Set();
    }

    const nextVisiting = new Set(visiting);

    nextVisiting.add(key);

    const relation = await getRelation(schemaName, viewName);

    if (!relation || !["v", "m"].includes(relation.relkind) || !relation.definition) {
      return new Set();
    }

    const result = new Set();

    /**
     * Preserve any actual catalog-level NOT NULL metadata.
     */
    for (const column of relation.orderedColumns) {
      if (column.notNull) {
        result.add(column.name);
      }
    }

    let tree;

    try {
      tree = await unwrapParseResult(parser.parse(relation.definition));
    } catch (error) {
      verbose(`Could not parse ${key}: ${String(error)}`);

      viewInferenceCache.set(key, result);

      return result;
    }

    if (tree.stmts.length !== 1 || !tree.stmts[0]?.stmt) {
      verbose(`Skipping ${key}: expected one SELECT statement`);

      viewInferenceCache.set(key, result);

      return result;
    }

    const root = unwrapNode(tree.stmts[0].stmt);

    if (root.type !== "SelectStmt") {
      verbose(`Skipping ${key}: definition is not SELECT`);

      viewInferenceCache.set(key, result);

      return result;
    }

    const select = root.node;

    /**
     * UNION / INTERSECT / EXCEPT require separate nullability
     * analysis and are therefore intentionally skipped.
     */
    if (!isPlainSelect(select)) {
      verbose(`Skipping set-operation view ${key}`);

      viewInferenceCache.set(key, result);

      return result;
    }

    const targetList = select.targetList ?? [];

    /**
     * Associate SELECT targets with actual view columns by ordinal.
     *
     * pg_get_viewdef() normally returns the rewritten view target
     * list, making ordinal matching reliable.
     */
    if (targetList.length !== relation.orderedColumns.length) {
      verbose(
        `Skipping ${key}: ` +
          `target-list length (${targetList.length}) ` +
          `does not match view-column count ` +
          `(${relation.orderedColumns.length})`,
      );

      viewInferenceCache.set(key, result);

      return result;
    }

    const context = {
      sources: [],
      hasUnknownSources: false,
    };

    for (const fromNode of select.fromClause ?? []) {
      collectSources(fromNode, false, context);
    }

    for (let index = 0; index < targetList.length; index++) {
      const target = unwrapNode(targetList[index]);

      if (target.type !== "ResTarget") {
        continue;
      }

      const outputColumn = relation.orderedColumns[index];

      if (!outputColumn) {
        continue;
      }

      const provenNotNull = await expressionIsProvenNotNull(target.node.val, context, nextVisiting);

      if (provenNotNull) {
        result.add(outputColumn.name);

        verbose(`NOT NULL: ${schemaName}.${viewName}.${outputColumn.name}`);
      }
    }

    viewInferenceCache.set(key, result);

    return result;
  }

  /**
   * ---------------------------------------------------------
   * Expression inference
   * ---------------------------------------------------------
   */

  async function expressionIsProvenNotNull(expression, context, visiting) {
    if (!expression) {
      return false;
    }

    const expressionNode = unwrapNode(expression);

    /**
     * Conservative rule:
     *
     * Only direct column references inherit NOT NULL.
     *
     * For example:
     *
     *   SELECT t.id
     *
     * can inherit NOT NULL.
     *
     * But:
     *
     *   SELECT lower(t.name)
     *   SELECT t.id::text
     *   SELECT COALESCE(...)
     *   SELECT CASE ...
     *
     * are deliberately left untouched.
     */
    if (expressionNode.type !== "ColumnRef") {
      return false;
    }

    const parts = getColumnRefParts(expressionNode.node);

    if (!parts) {
      return false;
    }

    const resolved = await resolveColumnReference(parts, context);

    if (!resolved) {
      return false;
    }

    const { source, column } = resolved;

    /**
     * A NOT NULL source column may still become NULL when it
     * is on the nullable side of an outer join.
     */
    if (source.nullableByJoin) {
      return false;
    }

    if (!source.schema) {
      return false;
    }

    return isRelationColumnNotNull(source.schema, source.relation, column, visiting);
  }

  /**
   * ---------------------------------------------------------
   * Resolve ColumnRef
   * ---------------------------------------------------------
   */

  async function resolveColumnReference(parts, context) {
    /**
     * column
     */
    if (parts.length === 1) {
      if (context.hasUnknownSources) {
        return null;
      }

      const column = parts[0];

      const candidates = [];

      for (const source of context.sources) {
        if (await relationHasColumn(source, column)) {
          candidates.push(source);
        }
      }

      /**
       * An unqualified column reference is only safe when it
       * resolves to exactly one source.
       */
      if (candidates.length !== 1) {
        return null;
      }

      return {
        source: candidates[0],

        column,
      };
    }

    /**
     * table.column
     *
     * or
     *
     * alias.column
     */
    if (parts.length === 2) {
      const [qualifier, column] = parts;

      const candidates = context.sources.filter((source) => source.alias === qualifier);

      if (candidates.length !== 1) {
        return null;
      }

      return {
        source: candidates[0],

        column,
      };
    }

    /**
     * schema.table.column
     */
    if (parts.length === 3) {
      const [schemaName, relationName, column] = parts;

      const candidates = context.sources.filter(
        (source) =>
          !source.hasExplicitAlias &&
          source.schema === schemaName &&
          source.relation === relationName,
      );

      if (candidates.length !== 1) {
        return null;
      }

      return {
        source: candidates[0],

        column,
      };
    }

    /**
     * Database-qualified references and other unusual cases
     * are intentionally not inferred.
     */
    return null;
  }

  /**
   * ---------------------------------------------------------
   * FROM/JOIN analysis
   * ---------------------------------------------------------
   */

  function collectSources(wrappedNode, inheritedNullable, context) {
    if (!wrappedNode) {
      return;
    }

    const current = unwrapNode(wrappedNode);

    /**
     * FROM schema.table
     */
    if (current.type === "RangeVar") {
      const relation = current.node;

      const alias = readAlias(relation.alias);

      /**
       * PostgreSQL supports:
       *
       *   FROM table AS t(a, b, c)
       *
       * This changes the visible column names. Rather than trying
       * to map them, skip inference for ambiguous references.
       */
      if (alias.columns.length > 0) {
        context.hasUnknownSources = true;

        return;
      }

      context.sources.push({
        schema: relation.schemaname ?? null,

        relation: relation.relname,

        alias: alias.name ?? relation.relname,

        hasExplicitAlias: Boolean(alias.name),

        nullableByJoin: inheritedNullable,
      });

      return;
    }

    /**
     * JOIN
     */
    if (current.type === "JoinExpr") {
      const join = current.node;

      const joinAlias = readAlias(join.alias);

      /**
       * An alias over an entire joined relation changes namespace
       * visibility significantly. Stay conservative.
       */
      if (joinAlias.name || joinAlias.columns.length > 0) {
        context.hasUnknownSources = true;

        return;
      }

      const nullableSides = getJoinNullableSides(join.jointype);

      if (!nullableSides) {
        context.hasUnknownSources = true;

        return;
      }

      collectSources(
        join.larg,

        inheritedNullable || nullableSides.left,

        context,
      );

      collectSources(
        join.rarg,

        inheritedNullable || nullableSides.right,

        context,
      );

      return;
    }

    /**
     * Examples deliberately treated as unknown:
     *
     * RangeSubselect
     * RangeFunction
     * RangeTableFunc
     * complex CTE-related constructs
     */
    context.hasUnknownSources = true;
  }

  function getJoinNullableSides(joinType) {
    switch (joinType) {
      /**
       * INNER JOIN
       */
      case 0:
      case "JOIN_INNER":
        return {
          left: false,
          right: false,
        };

      /**
       * LEFT JOIN
       */
      case 1:
      case "JOIN_LEFT":
        return {
          left: false,
          right: true,
        };

      /**
       * FULL JOIN
       */
      case 2:
      case "JOIN_FULL":
        return {
          left: true,
          right: true,
        };

      /**
       * RIGHT JOIN
       */
      case 3:
      case "JOIN_RIGHT":
        return {
          left: true,
          right: false,
        };

      default:
        return null;
    }
  }

  /**
   * ---------------------------------------------------------
   * AST helpers
   * ---------------------------------------------------------
   */

  function isPlainSelect(select) {
    return (
      select.op === undefined || select.op === null || select.op === 0 || select.op === "SETOP_NONE"
    );
  }

  function getColumnRefParts(columnRef) {
    const fields = columnRef.fields ?? [];

    const result = [];

    for (const field of fields) {
      const item = unwrapNode(field);

      /**
       * A_Star means SELECT table.*.
       * pg_get_viewdef normally expands it, but do not infer if it
       * remains in the parsed representation.
       */
      if (item.type !== "String") {
        return null;
      }

      result.push(item.node.sval);
    }

    return result.length > 0 ? result : null;
  }

  function readAlias(aliasValue) {
    if (!aliasValue) {
      return {
        name: null,
        columns: [],
      };
    }

    let alias = aliasValue;

    /**
     * Depending on parser/version, Alias may be embedded directly
     * or wrapped as a normal AST node.
     */
    try {
      const unwrapped = unwrapNode(aliasValue);

      if (unwrapped.type === "Alias") {
        alias = unwrapped.node;
      }
    } catch {
      /**
       * Embedded Alias.
       */
    }

    const columns = [];

    for (const column of alias.colnames ?? []) {
      try {
        const value = unwrapNode(column);

        if (value.type === "String") {
          columns.push(value.node.sval);
        }
      } catch {
        /**
         * Unknown alias-column representation.
         *
         * Return a marker so inference becomes conservative.
         */
        return {
          name: alias.aliasname ?? null,

          columns: ["__unknown__"],
        };
      }
    }

    return {
      name: alias.aliasname ?? null,

      columns,
    };
  }

  /**
   * ---------------------------------------------------------
   * Validate requested schemas
   * ---------------------------------------------------------
   */

  const schemaResult = await client.query(
    `
        SELECT nspname
        FROM pg_catalog.pg_namespace
        WHERE nspname = ANY($1::text[])
      `,
    [options.schemas],
  );

  const existingSchemas = new Set(schemaResult.rows.map((row) => row.nspname));

  const missingSchemas = options.schemas.filter((schemaName) => !existingSchemas.has(schemaName));

  if (missingSchemas.length > 0) {
    throw new Error(
      `Schema(s) not found: ${missingSchemas.join(", ")}\n` + formatDatabaseInfo(databaseInfo),
    );
  }

  /**
   * ---------------------------------------------------------
   * Find views across ALL requested schemas
   * ---------------------------------------------------------
   */

  const viewsResult = await client.query(
    `
        SELECT
          n.nspname AS schema_name,
          c.relname AS view_name

        FROM pg_catalog.pg_class AS c

        JOIN pg_catalog.pg_namespace AS n
          ON n.oid = c.relnamespace

        WHERE n.nspname = ANY($1::text[])
          AND c.relkind = 'v'

        ORDER BY
          n.nspname,
          c.relname
      `,
    [options.schemas],
  );

  if (viewsResult.rowCount === 0) {
    throw new Error(
      `No views found in requested schemas: ` +
        `${options.schemas.join(", ")}\n` +
        formatDatabaseInfo(databaseInfo),
    );
  }

  /**
   * Structure:
   *
   * Map<
   *   schemaName,
   *   Map<
   *     viewName,
   *     string[]
   *   >
   * >
   */
  const overrides = new Map();

  for (const row of viewsResult.rows) {
    const schemaName = row.schema_name;

    const viewName = row.view_name;

    const notNullColumns = await inferViewColumns(schemaName, viewName);

    if (notNullColumns.size === 0) {
      continue;
    }

    let schemaOverrides = overrides.get(schemaName);

    if (!schemaOverrides) {
      schemaOverrides = new Map();

      overrides.set(schemaName, schemaOverrides);
    }

    schemaOverrides.set(viewName, [...notNullColumns].sort());
  }

  /**
   * ---------------------------------------------------------
   * Render TypeScript
   * ---------------------------------------------------------
   */

  const inputText = await readFile(options.input, "utf8");

  const outputText = renderDatabaseType({
    inputText,

    overrides,
  });

  await writeFile(options.input, outputText, "utf8");

  await client.query("COMMIT");

  let correctedViewCount = 0;
  let columnCount = 0;

  for (const views of overrides.values()) {
    correctedViewCount += views.size;

    for (const columns of views.values()) {
      columnCount += columns.length;
    }
  }

  console.log(`Updated ${options.input}`);

  console.log(`Schemas: ${options.schemas.join(", ")}`);

  console.log(`Views inspected: ${viewsResult.rowCount}`);

  console.log(`Views with inferred NOT NULL columns: ${correctedViewCount}`);

  console.log(`Proven NOT NULL view columns: ${columnCount}`);

  if (options.verbose) {
    console.log(formatDatabaseInfo(databaseInfo));
  }

  /**
   * ---------------------------------------------------------
   * Local helpers needing client/options
   * ---------------------------------------------------------
   */

  async function getDatabaseInfo() {
    const result = await client.query(
      `
          SELECT
            current_database() AS database,
            current_user AS username,
            inet_server_addr()::text AS host,
            inet_server_port() AS port
        `,
    );

    return result.rows[0];
  }

  async function getPostgresMajorVersion() {
    const result = await client.query("SHOW server_version_num");

    const versionNumber = Number(result.rows[0]?.server_version_num);

    if (!Number.isFinite(versionNumber)) {
      throw new Error("Could not determine PostgreSQL version.");
    }

    return Math.floor(versionNumber / 10_000);
  }

  function verbose(message) {
    if (options.verbose) {
      console.log(message);
    }
  }
} catch (error) {
  try {
    await client.query("ROLLBACK");
  } catch {
    /**
     * Ignore rollback errors.
     */
  }

  console.error(error instanceof Error ? error.message : String(error));

  process.exitCode = 1;
} finally {
  await client.end();
}

/**
 * ===========================================================
 * TypeScript output generator
 * ===========================================================
 */

function renderDatabaseType({ inputText, overrides }) {
  const generatedTypes = removePreviousEnrichment(inputText);
  // All generated helpers (Tables, TablesInsert, TablesUpdate, Enums, and
  // CompositeTypes) derive from this alias. An export rename alone does not
  // change their local references to the original Database type.
  const internalDatabaseTypes = removeDatabaseExport(generatedTypes).replace(
    /(^type\s+DatabaseWithoutInternals\s*=\s*Omit<\s*)Database\b/m,
    "$1DatabaseWithNotNullViews",
  );
  const lines = [enrichmentBanner, ""];

  /**
   * No corrections found.
   */
  if (overrides.size === 0) {
    lines.push(
      "type DatabaseWithNotNullViews = Database;",
      "export type { DatabaseWithNotNullViews as Database };",
    );

    return `${internalDatabaseTypes.trimEnd()}\n\n${lines.join("\n")}\n`;
  }

  lines.push(
    `import type { MergeDeep } from "type-fest";`,
    "",
    "type DatabaseWithNotNullViews = MergeDeep<",
    "  Database,",
    "  {",
  );

  for (const [schemaName, views] of overrides) {
    lines.push(`    ${JSON.stringify(schemaName)}: {`, "      Views: {");

    for (const [viewName, columns] of views) {
      lines.push(`        ${JSON.stringify(viewName)}: {`, "          Row: {");

      for (const column of columns) {
        const originalType =
          "Database" +
          `[${JSON.stringify(schemaName)}]` +
          `["Views"]` +
          `[${JSON.stringify(viewName)}]` +
          `["Row"]` +
          `[${JSON.stringify(column)}]`;

        lines.push(`            ${JSON.stringify(column)}: NonNullable<${originalType}>;`);
      }

      lines.push("          };", "        };");
    }

    lines.push("      };", "    };");
  }

  lines.push("  }", ">;", "");
  lines.push("export type { DatabaseWithNotNullViews as Database };");

  return `${internalDatabaseTypes.trimEnd()}\n\n${lines.join("\n")}\n`;
}

function removePreviousEnrichment(inputText) {
  const bannerIndex = inputText.indexOf(enrichmentBanner);

  return bannerIndex === -1 ? inputText : inputText.slice(0, bannerIndex);
}

function removeDatabaseExport(inputText) {
  const exportedDatabasePattern = /^export\s+(type\s+Database\s*=)/m;

  if (exportedDatabasePattern.test(inputText)) {
    return inputText.replace(exportedDatabasePattern, "$1");
  }

  if (/^type\s+Database\s*=/m.test(inputText)) {
    return inputText;
  }

  throw new Error('Could not find "export type Database = ..." in the input file.');
}

/**
 * ===========================================================
 * CLI argument handling
 * ===========================================================
 */

function parseArgs(argv) {
  const options = {
    schemas: null,

    input: "src/database-generated.types.ts",

    dbUrl: null,

    verbose: false,
  };

  const schemaValues = [];

  for (let index = 0; index < argv.length; index++) {
    const argument = argv[index];

    switch (argument) {
      case "--schema": {
        const value = requireValue(argv, ++index, "--schema");

        schemaValues.push(...parseSchemaList(value));

        break;
      }

      case "--input":
        options.input = requireValue(argv, ++index, "--input");
        break;

      case "--db-url":
        options.dbUrl = requireValue(argv, ++index, "--db-url");
        break;

      case "--verbose":
        options.verbose = true;
        break;

      case "--help":
      case "-h":
        printHelp();
        process.exit(0);
        break;

      default:
        throw new Error(`Unknown argument: ${argument}`);
    }
  }

  options.schemas = schemaValues.length > 0 ? unique(schemaValues) : ["api"];

  return options;
}

function parseSchemaList(value) {
  const schemas = value
    .split(",")
    .map((schema) => schema.trim())
    .filter(Boolean);

  if (schemas.length === 0) {
    throw new Error("--schema requires at least one schema name.");
  }

  return schemas;
}

function requireValue(argv, index, option) {
  const value = argv[index];

  if (!value || value.startsWith("--")) {
    throw new Error(`${option} requires a value.`);
  }

  return value;
}

function printHelp() {
  console.log(`
Generate corrected Supabase TypeScript types for NOT NULL view columns.

Usage:

  node scripts/generate-supabase-view-types-with-not-nulls.mjs [options]

Options:

  --schema <schemas>
      Comma-separated schemas to process.

      Default:
        api

      Examples:
        --schema api
        --schema api,service_api

  --input <file>
      Supabase generated type file to update in place.

      Default:
        src/database-generated.types.ts

  --db-url <url>
      PostgreSQL connection URL.

      If omitted, DATABASE_URL or SUPABASE_DB_URL is used.

  --verbose
      Print inferred columns and diagnostic information.

  --help
      Show this help.
`);
}

/**
 * ===========================================================
 * Generic helpers
 * ===========================================================
 */

function relationKey(schemaName, relationName) {
  return `${schemaName}.` + relationName;
}

function unique(values) {
  return [...new Set(values)];
}

function formatDatabaseInfo(info) {
  return (
    "Connected to: " +
    `database=${info.database}, ` +
    `user=${info.username}, ` +
    `host=${info.host}, ` +
    `port=${info.port}`
  );
}

Dominant language
TypeScript
Stars
2.4k
Forks
523
Avg merge
1d 1h
Merged PRs (30d)
268

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from supabase/cli

All issues in supabase/cli

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.