From 3704cad53a51abf9ed29567ddf2203d7084536e1 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 7 Mar 2025 17:20:37 +0100 Subject: [PATCH] added BaseIntrospector and reorganized connections --- .../connection/SqliteIntrospector.spec.ts | 9 +- app/src/data/connection/BaseIntrospector.ts | 75 +++++++++ app/src/data/connection/Connection.ts | 9 +- app/src/data/connection/DummyConnection.ts | 6 +- app/src/data/connection/SqliteIntrospector.ts | 151 ------------------ app/src/data/connection/index.ts | 12 ++ .../connection/postgres/PostgresConnection.ts | 5 + .../postgres/PostgresIntrospector.ts | 110 +++---------- .../{ => sqlite}/LibsqlConnection.ts | 6 +- .../{ => sqlite}/SqliteConnection.ts | 2 +- .../connection/sqlite/SqliteIntrospector.ts | 95 +++++++++++ .../{ => sqlite}/SqliteLocalConnection.ts | 0 app/src/data/index.ts | 6 +- app/src/data/schema/SchemaManager.ts | 2 +- app/src/media/MediaField.ts | 2 +- 15 files changed, 232 insertions(+), 258 deletions(-) create mode 100644 app/src/data/connection/BaseIntrospector.ts delete mode 100644 app/src/data/connection/SqliteIntrospector.ts create mode 100644 app/src/data/connection/index.ts rename app/src/data/connection/{ => sqlite}/LibsqlConnection.ts (93%) rename app/src/data/connection/{ => sqlite}/SqliteConnection.ts (97%) create mode 100644 app/src/data/connection/sqlite/SqliteIntrospector.ts rename app/src/data/connection/{ => sqlite}/SqliteLocalConnection.ts (100%) diff --git a/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts b/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts index 84e4a29..ee46b7b 100644 --- a/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts +++ b/app/__test__/data/specs/connection/SqliteIntrospector.spec.ts @@ -1,8 +1,7 @@ -import { beforeEach, describe, test, expect } from "bun:test"; -import { SqliteIntrospector } from "data/connection/SqliteIntrospector"; -import { getDummyConnection, getDummyDatabase } from "../../helper"; -import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely"; -import { _jsonp } from "core/utils"; +import { describe, expect, test } from "bun:test"; +import { SqliteIntrospector } from "data/connection"; +import { getDummyDatabase } from "../../helper"; +import { Kysely, SqliteDialect } from "kysely"; function create() { const database = getDummyDatabase().dummyDb; diff --git a/app/src/data/connection/BaseIntrospector.ts b/app/src/data/connection/BaseIntrospector.ts new file mode 100644 index 0000000..e96a44d --- /dev/null +++ b/app/src/data/connection/BaseIntrospector.ts @@ -0,0 +1,75 @@ +import { + type DatabaseMetadata, + type DatabaseMetadataOptions, + type Kysely, + type KyselyPlugin, + type RawBuilder, + type TableMetadata, + type DatabaseIntrospector, + type SchemaMetadata, + ParseJSONResultsPlugin, + DEFAULT_MIGRATION_TABLE, + DEFAULT_MIGRATION_LOCK_TABLE, +} from "kysely"; +import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner"; +import type { IndexMetadata } from "data/connection/Connection"; + +export type TableSpec = TableMetadata & { + indices: IndexMetadata[]; +}; +export type SchemaSpec = TableSpec[]; + +export type BaseIntrospectorConfig = { + excludeTables?: string[]; + plugins?: KyselyPlugin[]; +}; + +export abstract class BaseIntrospector implements DatabaseIntrospector { + readonly _excludeTables: string[] = []; + readonly _plugins: KyselyPlugin[]; + + constructor( + protected readonly db: Kysely, + config: BaseIntrospectorConfig = {}, + ) { + this._excludeTables = config.excludeTables ?? []; + this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()]; + } + + abstract getSchemaSpec(): Promise; + abstract getSchemas(): Promise; + + protected getExcludedTableNames(): string[] { + return [...this._excludeTables, DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE]; + } + + protected async executeWithPlugins(query: RawBuilder): Promise { + const result = await query.execute(this.db); + const runner = new KyselyPluginRunner(this._plugins ?? []); + return (await runner.transformResultRows(result.rows)) as unknown as T; + } + + async getMetadata(options?: DatabaseMetadataOptions): Promise { + return { + tables: await this.getTables(options), + }; + } + + async getIndices(tbl_name?: string): Promise { + const schema = await this.getSchemaSpec(); + return schema + .flatMap((table) => table.indices) + .filter((index) => !tbl_name || index.table === tbl_name); + } + + async getTables( + options: DatabaseMetadataOptions = { withInternalKyselyTables: false }, + ): Promise { + const schema = await this.getSchemaSpec(); + return schema.map((table) => ({ + name: table.name, + isView: table.isView, + columns: table.columns, + })); + } +} diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts index c80679b..c1ea50a 100644 --- a/app/src/data/connection/Connection.ts +++ b/app/src/data/connection/Connection.ts @@ -13,6 +13,7 @@ import { type Simplify, sql, } from "kysely"; +import type { BaseIntrospector } from "./BaseIntrospector"; export type QB = SelectQueryBuilder; @@ -23,10 +24,6 @@ export type IndexMetadata = { columns: { name: string; order: number }[]; }; -export interface ConnectionIntrospector extends DatabaseIntrospector { - getIndices(tbl_name?: string): Promise; -} - export interface SelectQueryBuilderExpression extends AliasableExpression { get isSelectQueryBuilder(): true; toOperationNode(): SelectQueryNode; @@ -100,8 +97,8 @@ export abstract class Connection { return conn[CONN_SYMBOL] === true; } - getIntrospector(): ConnectionIntrospector { - return this.kysely.introspection as ConnectionIntrospector; + getIntrospector(): BaseIntrospector { + return this.kysely.introspection as any; } supportsBatching(): boolean { diff --git a/app/src/data/connection/DummyConnection.ts b/app/src/data/connection/DummyConnection.ts index 451575d..9f7287a 100644 --- a/app/src/data/connection/DummyConnection.ts +++ b/app/src/data/connection/DummyConnection.ts @@ -1,7 +1,11 @@ -import { Connection } from "./Connection"; +import { Connection, type FieldSpec, type SchemaResponse } from "./Connection"; export class DummyConnection extends Connection { constructor() { super(undefined as any); } + + override getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse { + throw new Error("Method not implemented."); + } } diff --git a/app/src/data/connection/SqliteIntrospector.ts b/app/src/data/connection/SqliteIntrospector.ts deleted file mode 100644 index 985e1cd..0000000 --- a/app/src/data/connection/SqliteIntrospector.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - type DatabaseIntrospector, - type DatabaseMetadata, - type DatabaseMetadataOptions, - type Kysely, - ParseJSONResultsPlugin, - type SchemaMetadata, - type TableMetadata, - type KyselyPlugin, -} from "kysely"; -import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely"; -import type { ConnectionIntrospector, IndexMetadata } from "./Connection"; -import { KyselyPluginRunner } from "data"; - -export type SqliteIntrospectorConfig = { - excludeTables?: string[]; - plugins?: KyselyPlugin[]; -}; - -export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntrospector { - readonly #db: Kysely; - readonly _excludeTables: string[] = []; - readonly _plugins: KyselyPlugin[]; - - constructor(db: Kysely, config: SqliteIntrospectorConfig = {}) { - this.#db = db; - this._excludeTables = config.excludeTables ?? []; - this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()]; - } - - async getSchemas(): Promise { - // Sqlite doesn't support schemas. - return []; - } - - async getSchema() { - const excluded = [ - ...this._excludeTables, - DEFAULT_MIGRATION_TABLE, - DEFAULT_MIGRATION_LOCK_TABLE, - ]; - const query = sql` - SELECT m.name, m.type, m.sql, - (SELECT json_group_array( - json_object( - 'name', p.name, - 'type', p.type, - 'notnull', p."notnull", - 'default', p.dflt_value, - 'primary_key', p.pk - )) FROM pragma_table_info(m.name) p) AS columns, - (SELECT json_group_array( - json_object( - 'name', i.name, - 'origin', i.origin, - 'partial', i.partial, - 'sql', im.sql, - 'columns', (SELECT json_group_array( - json_object( - 'name', ii.name, - 'seqno', ii.seqno - )) FROM pragma_index_info(i.name) ii) - )) FROM pragma_index_list(m.name) i - LEFT JOIN sqlite_master im ON im.name = i.name - AND im.type = 'index' - ) AS indices - FROM sqlite_master m - WHERE m.type IN ('table', 'view') - and m.name not like 'sqlite_%' - and m.name not in (${excluded.join(", ")}) - `; - - const result = await query.execute(this.#db); - const runner = new KyselyPluginRunner(this._plugins ?? []); - const tables = (await runner.transformResultRows(result.rows)) as unknown as { - name: string; - type: string; - sql: string; - columns: { - name: string; - type: string; - notnull: number; - dflt_value: any; - pk: number; - }[]; - indices: { - name: string; - origin: string; - partial: number; - sql: string; - columns: { name: string; seqno: number }[]; - }[]; - }[]; - - //console.log("tables", tables); - return tables.map((table) => ({ - name: table.name, - isView: table.type === "view", - columns: table.columns.map((col) => { - const autoIncrementCol = table.sql - ?.split(/[\(\),]/) - ?.find((it) => it.toLowerCase().includes("autoincrement")) - ?.trimStart() - ?.split(/\s+/)?.[0] - ?.replace(/["`]/g, ""); - - return { - name: col.name, - dataType: col.type, - isNullable: !col.notnull, - isAutoIncrementing: col.name === autoIncrementCol, - hasDefaultValue: col.dflt_value != null, - comment: undefined, - }; - }), - indices: table.indices.map((index) => ({ - name: index.name, - table: table.name, - isUnique: index.sql?.match(/unique/i) != null, - columns: index.columns.map((col) => ({ - name: col.name, - order: col.seqno, - })), - })), - })); - } - - async getMetadata(options?: DatabaseMetadataOptions): Promise { - return { - tables: await this.getTables(options), - }; - } - - async getIndices(tbl_name?: string): Promise { - const schema = await this.getSchema(); - return schema - .flatMap((table) => table.indices) - .filter((index) => !tbl_name || index.table === tbl_name); - } - - async getTables( - options: DatabaseMetadataOptions = { withInternalKyselyTables: false }, - ): Promise { - const schema = await this.getSchema(); - return schema.map((table) => ({ - name: table.name, - isView: table.isView, - columns: table.columns, - })); - } -} diff --git a/app/src/data/connection/index.ts b/app/src/data/connection/index.ts new file mode 100644 index 0000000..155be94 --- /dev/null +++ b/app/src/data/connection/index.ts @@ -0,0 +1,12 @@ +export { Connection } from "./Connection"; +export { BaseIntrospector } from "./BaseIntrospector"; + +// sqlite +export { LibsqlConnection, type LibSqlCredentials } from "./sqlite/LibsqlConnection"; +export { SqliteConnection } from "./sqlite/SqliteConnection"; +export { SqliteLocalConnection } from "./sqlite/SqliteLocalConnection"; +export { SqliteIntrospector } from "./sqlite/SqliteIntrospector"; + +// postgres +export { PostgresConnection, type PostgresConnectionConfig } from "./postgres/PostgresConnection"; +export { PostgresIntrospector } from "./postgres/PostgresIntrospector"; diff --git a/app/src/data/connection/postgres/PostgresConnection.ts b/app/src/data/connection/postgres/PostgresConnection.ts index a7d29e0..812ebee 100644 --- a/app/src/data/connection/postgres/PostgresConnection.ts +++ b/app/src/data/connection/postgres/PostgresConnection.ts @@ -42,11 +42,16 @@ export class PostgresConnection extends Connection { let type: ColumnDataType = spec.primary ? "serial" : spec.type; switch (spec.type) { + case "blob": + type = "bytea"; + break; case "date": case "datetime": + // https://www.postgresql.org/docs/17/datatype-datetime.html type = "timestamp"; break; case "text": + // https://www.postgresql.org/docs/17/datatype-character.html type = "varchar"; break; } diff --git a/app/src/data/connection/postgres/PostgresIntrospector.ts b/app/src/data/connection/postgres/PostgresIntrospector.ts index d41fc7c..82d6aaa 100644 --- a/app/src/data/connection/postgres/PostgresIntrospector.ts +++ b/app/src/data/connection/postgres/PostgresIntrospector.ts @@ -1,55 +1,37 @@ -import { - type DatabaseIntrospector, - type DatabaseMetadata, - type DatabaseMetadataOptions, - type SchemaMetadata, - type TableMetadata, - type Kysely, - type KyselyPlugin, - ParseJSONResultsPlugin, -} from "kysely"; -import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely"; -import { KyselyPluginRunner } from "data"; -import type { IndexMetadata } from "data/connection/Connection"; +import { type SchemaMetadata, sql } from "kysely"; +import { BaseIntrospector } from "data/connection/BaseIntrospector"; -export type PostgresIntrospectorConfig = { - excludeTables?: string[]; - plugins?: KyselyPlugin[]; +type PostgresSchemaSpec = { + name: string; + type: "VIEW" | "BASE TABLE"; + columns: { + name: string; + type: string; + notnull: number; + dflt: string; + pk: boolean; + }[]; + indices: { + name: string; + origin: string; + partial: number; + sql: string; + columns: { name: string; seqno: number }[]; + }[]; }; -export class PostgresIntrospector implements DatabaseIntrospector { - readonly #db: Kysely; - readonly _excludeTables: string[] = []; - readonly _plugins: KyselyPlugin[]; - - constructor(db: Kysely, config: PostgresIntrospectorConfig = {}) { - this.#db = db; - this._excludeTables = config.excludeTables ?? []; - this._plugins = config.plugins ?? [new ParseJSONResultsPlugin()]; - } - +export class PostgresIntrospector extends BaseIntrospector { async getSchemas(): Promise { - const rawSchemas = await this.#db + const rawSchemas = await this.db .selectFrom("pg_catalog.pg_namespace") .select("nspname") - .$castTo() + .$castTo<{ nspname: string }>() .execute(); return rawSchemas.map((it) => ({ name: it.nspname })); } - async getMetadata(options?: DatabaseMetadataOptions): Promise { - return { - tables: await this.getTables(options), - }; - } - - async getSchema() { - const excluded = [ - ...this._excludeTables, - DEFAULT_MIGRATION_TABLE, - DEFAULT_MIGRATION_LOCK_TABLE, - ]; + async getSchemaSpec() { const query = sql` WITH tables_and_views AS ( SELECT table_name AS name, @@ -58,7 +40,7 @@ export class PostgresIntrospector implements DatabaseIntrospector { WHERE table_schema = 'public' AND table_type IN ('BASE TABLE', 'VIEW') AND table_name NOT LIKE 'pg_%' - AND table_name NOT IN (${excluded.join(", ")}) + AND table_name NOT IN (${this.getExcludedTableNames().join(", ")}) ), columns_info AS ( @@ -115,26 +97,7 @@ export class PostgresIntrospector implements DatabaseIntrospector { LEFT JOIN indices_info ii ON tv.name = ii.table_name; `; - const result = await query.execute(this.#db); - const runner = new KyselyPluginRunner(this._plugins ?? []); - const tables = (await runner.transformResultRows(result.rows)) as unknown as { - name: string; - type: "VIEW" | "BASE TABLE"; - columns: { - name: string; - type: string; - notnull: number; - dflt: string; - pk: boolean; - }[]; - indices: { - name: string; - origin: string; - partial: number; - sql: string; - columns: { name: string; seqno: number }[]; - }[]; - }[]; + const tables = await this.executeWithPlugins(query); return tables.map((table) => ({ name: table.name, @@ -144,6 +107,7 @@ export class PostgresIntrospector implements DatabaseIntrospector { name: col.name, dataType: col.type, isNullable: !col.notnull, + // @todo: check default value on 'nextval' see https://www.postgresql.org/docs/17/datatype-numeric.html#DATATYPE-SERIAL isAutoIncrementing: true, // just for now hasDefaultValue: col.dflt != null, comment: undefined, @@ -160,26 +124,4 @@ export class PostgresIntrospector implements DatabaseIntrospector { })), })); } - - async getIndices(tbl_name?: string): Promise { - const schema = await this.getSchema(); - return schema - .flatMap((table) => table.indices) - .filter((index) => !tbl_name || index.table === tbl_name); - } - - async getTables( - options: DatabaseMetadataOptions = { withInternalKyselyTables: false }, - ): Promise { - const schema = await this.getSchema(); - return schema.map((table) => ({ - name: table.name, - isView: table.isView, - columns: table.columns, - })); - } -} - -interface RawSchemaMetadata { - nspname: string; } diff --git a/app/src/data/connection/LibsqlConnection.ts b/app/src/data/connection/sqlite/LibsqlConnection.ts similarity index 93% rename from app/src/data/connection/LibsqlConnection.ts rename to app/src/data/connection/sqlite/LibsqlConnection.ts index a766931..9952054 100644 --- a/app/src/data/connection/LibsqlConnection.ts +++ b/app/src/data/connection/sqlite/LibsqlConnection.ts @@ -1,9 +1,9 @@ import { type Client, type Config, type InStatement, createClient } from "@libsql/client"; import { LibsqlDialect } from "@libsql/kysely-libsql"; import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely"; -import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin"; -import { KyselyPluginRunner } from "../plugins/KyselyPluginRunner"; -import type { QB } from "./Connection"; +import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin"; +import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner"; +import type { QB } from "../Connection"; import { SqliteConnection } from "./SqliteConnection"; import { SqliteIntrospector } from "./SqliteIntrospector"; diff --git a/app/src/data/connection/SqliteConnection.ts b/app/src/data/connection/sqlite/SqliteConnection.ts similarity index 97% rename from app/src/data/connection/SqliteConnection.ts rename to app/src/data/connection/sqlite/SqliteConnection.ts index 665eb44..ea24320 100644 --- a/app/src/data/connection/SqliteConnection.ts +++ b/app/src/data/connection/sqlite/SqliteConnection.ts @@ -1,6 +1,6 @@ import type { ColumnDataType, ColumnDefinitionBuilder, Kysely, KyselyPlugin } from "kysely"; import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite"; -import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "./Connection"; +import { Connection, type DbFunctions, type FieldSpec, type SchemaResponse } from "../Connection"; export class SqliteConnection extends Connection { constructor(kysely: Kysely, fn: Partial = {}, plugins: KyselyPlugin[] = []) { diff --git a/app/src/data/connection/sqlite/SqliteIntrospector.ts b/app/src/data/connection/sqlite/SqliteIntrospector.ts new file mode 100644 index 0000000..f584050 --- /dev/null +++ b/app/src/data/connection/sqlite/SqliteIntrospector.ts @@ -0,0 +1,95 @@ +import { type SchemaMetadata, sql } from "kysely"; +import { BaseIntrospector } from "../BaseIntrospector"; + +export type SqliteSchemaSpec = { + name: string; + type: "table" | "view"; + sql: string; + columns: { + name: string; + type: string; + notnull: number; + dflt_value: any; + pk: number; + }[]; + indices: { + name: string; + origin: string; + partial: number; + sql: string; + columns: { name: string; seqno: number }[]; + }[]; +}; + +export class SqliteIntrospector extends BaseIntrospector { + async getSchemas(): Promise { + // Sqlite doesn't support schemas. + return []; + } + + async getSchemaSpec() { + const query = sql` + SELECT m.name, m.type, m.sql, + (SELECT json_group_array( + json_object( + 'name', p.name, + 'type', p.type, + 'notnull', p."notnull", + 'default', p.dflt_value, + 'primary_key', p.pk + )) FROM pragma_table_info(m.name) p) AS columns, + (SELECT json_group_array( + json_object( + 'name', i.name, + 'origin', i.origin, + 'partial', i.partial, + 'sql', im.sql, + 'columns', (SELECT json_group_array( + json_object( + 'name', ii.name, + 'seqno', ii.seqno + )) FROM pragma_index_info(i.name) ii) + )) FROM pragma_index_list(m.name) i + LEFT JOIN sqlite_master im ON im.name = i.name + AND im.type = 'index' + ) AS indices + FROM sqlite_master m + WHERE m.type IN ('table', 'view') + and m.name not like 'sqlite_%' + and m.name not in (${this.getExcludedTableNames().join(", ")}) + `; + + const tables = await this.executeWithPlugins(query); + + return tables.map((table) => ({ + name: table.name, + isView: table.type === "view", + columns: table.columns.map((col) => { + const autoIncrementCol = table.sql + ?.split(/[\(\),]/) + ?.find((it) => it.toLowerCase().includes("autoincrement")) + ?.trimStart() + ?.split(/\s+/)?.[0] + ?.replace(/["`]/g, ""); + + return { + name: col.name, + dataType: col.type, + isNullable: !col.notnull, + isAutoIncrementing: col.name === autoIncrementCol, + hasDefaultValue: col.dflt_value != null, + comment: undefined, + }; + }), + indices: table.indices.map((index) => ({ + name: index.name, + table: table.name, + isUnique: index.sql?.match(/unique/i) != null, + columns: index.columns.map((col) => ({ + name: col.name, + order: col.seqno, + })), + })), + })); + } +} diff --git a/app/src/data/connection/SqliteLocalConnection.ts b/app/src/data/connection/sqlite/SqliteLocalConnection.ts similarity index 100% rename from app/src/data/connection/SqliteLocalConnection.ts rename to app/src/data/connection/sqlite/SqliteLocalConnection.ts diff --git a/app/src/data/index.ts b/app/src/data/index.ts index 165436f..eb3e893 100644 --- a/app/src/data/index.ts +++ b/app/src/data/index.ts @@ -5,6 +5,7 @@ export * from "./entities"; export * from "./relations"; export * from "./schema/SchemaManager"; export * from "./prototype"; +export * from "./connection"; export { type RepoQuery, @@ -14,11 +15,6 @@ export { whereSchema, } from "./server/data-query-impl"; -export { Connection } from "./connection/Connection"; -export { LibsqlConnection, type LibSqlCredentials } from "./connection/LibsqlConnection"; -export { SqliteConnection } from "./connection/SqliteConnection"; -export { SqliteLocalConnection } from "./connection/SqliteLocalConnection"; -export { SqliteIntrospector } from "./connection/SqliteIntrospector"; export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner"; export { constructEntity, constructRelation } from "./schema/constructor"; diff --git a/app/src/data/schema/SchemaManager.ts b/app/src/data/schema/SchemaManager.ts index c9bebc1..d0cf2b2 100644 --- a/app/src/data/schema/SchemaManager.ts +++ b/app/src/data/schema/SchemaManager.ts @@ -1,4 +1,4 @@ -import type { AlterTableColumnAlteringBuilder, CompiledQuery, TableMetadata } from "kysely"; +import type { CompiledQuery, TableMetadata } from "kysely"; import type { IndexMetadata, SchemaResponse } from "../connection/Connection"; import type { Entity, EntityManager } from "../entities"; import { PrimaryField } from "../fields"; diff --git a/app/src/media/MediaField.ts b/app/src/media/MediaField.ts index d865f59..66fd0b6 100644 --- a/app/src/media/MediaField.ts +++ b/app/src/media/MediaField.ts @@ -47,7 +47,7 @@ export class MediaField< return this.config.min_items; } - schema() { + override schema() { return undefined; }