Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

supabase gen types ignores NOT NULL

オープン
#6,762 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
65/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
postgresql, supabase, typescript
領域
cli, databases, tooling

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

🐛 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}`
  );
}

主要言語
TypeScript
スター
2.4k
フォーク
523
平均マージ
1日 1時間
マージ済み PR(30日)
268

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

supabase/cli のほかの issue

supabase/cli の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。