UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

1,400 lines (1,388 loc) 86.9 kB
import { o as vRequired } from "./validators-C7LelqTN.js"; import { ConvexError, convexToJson, jsonToConvex, v } from "convex/values"; //#region src/aggregate-core/compare.ts function compareValues$1(k1, k2) { return compareAsTuples(makeComparable(k1), makeComparable(k2)); } function compareAsTuples(a, b) { if (a[0] === b[0]) return compareSameTypeValues(a[1], b[1]); if (a[0] < b[0]) return -1; return 1; } function compareSameTypeValues(v1, v2) { if (v1 === void 0 || v1 === null) return 0; if (typeof v1 === "bigint" || typeof v1 === "number" || typeof v1 === "boolean" || typeof v1 === "string") return v1 < v2 ? -1 : v1 === v2 ? 0 : 1; if (!Array.isArray(v1) || !Array.isArray(v2)) throw new Error(`Unexpected type ${v1}`); for (let i = 0; i < v1.length && i < v2.length; i++) { const cmp = compareAsTuples(v1[i], v2[i]); if (cmp !== 0) return cmp; } if (v1.length < v2.length) return -1; if (v1.length > v2.length) return 1; return 0; } function makeComparable(v) { if (v === void 0) return [0, void 0]; if (v === null) return [1, null]; if (typeof v === "bigint") return [2, v]; if (typeof v === "number") { if (Number.isNaN(v)) return [3.5, 0]; return [3, v]; } if (typeof v === "boolean") return [4, v]; if (typeof v === "string") return [5, v]; if (v instanceof ArrayBuffer) return [6, Array.from(new Uint8Array(v)).map(makeComparable)]; if (Array.isArray(v)) return [7, v.map(makeComparable)]; return [8, Object.keys(v).sort().map((k) => [k, v[k]]).map(makeComparable)]; } //#endregion //#region src/orm/builders/column-builder.ts /** * entityKind symbol for runtime type checking * Following Drizzle's pattern for type guards */ const entityKind = Symbol.for("kitcn:entityKind"); /** * Base ColumnBuilder abstract class * * All column builders inherit from this class. * Implements chaining methods and stores runtime config. */ var ColumnBuilder = class { static [entityKind] = "ColumnBuilder"; [entityKind] = "ColumnBuilder"; /** * Runtime configuration - actual mutable state */ config; constructor(name, dataType, columnType) { this.config = { name, notNull: false, default: void 0, hasDefault: false, primaryKey: false, isUnique: false, uniqueName: void 0, uniqueNulls: void 0, foreignKeyConfigs: [], dataType, columnType }; } /** * Mark column as NOT NULL * Returns type-branded instance with notNull: true */ notNull() { this.config.notNull = true; return this; } /** * Override the TypeScript type for this column. * Mirrors Drizzle's $type() (type-only, no runtime validation changes). */ $type() { return this; } /** * Set default value for column * Makes field optional on insert */ default(value) { this.config.default = value; this.config.hasDefault = true; return this; } /** * Set default function for column (runtime evaluated on insert). * Mirrors Drizzle's $defaultFn() / $default(). */ $defaultFn(fn) { this.config.defaultFn = fn; return this; } /** * Alias of $defaultFn for Drizzle parity. */ $default(fn) { return this.$defaultFn(fn); } /** * Set on-update function for column (runtime evaluated on update). * Mirrors Drizzle's $onUpdateFn() / $onUpdate(). */ $onUpdateFn(fn) { this.config.onUpdateFn = fn; return this; } /** * Alias of $onUpdateFn for Drizzle parity. */ $onUpdate(fn) { return this.$onUpdateFn(fn); } /** * Mark column as primary key * Implies NOT NULL */ primaryKey() { this.config.primaryKey = true; this.config.notNull = true; return this; } /** * Mark column as UNIQUE * Mirrors Drizzle column unique API */ unique(name, config) { this.config.isUnique = true; this.config.uniqueName = name; this.config.uniqueNulls = config?.nulls; return this; } /** * Define a foreign key reference * Mirrors Drizzle column references() API */ references(ref, config = {}) { this.config.foreignKeyConfigs.push({ ref, config }); return this; } }; //#endregion //#region src/orm/builders/convex-column-builder.ts /** * Convex-specific column builder base class * * All Convex column builders (ConvexTextBuilder, ConvexIntegerBuilder, etc.) * inherit from this class. */ var ConvexColumnBuilder = class extends ColumnBuilder { static [entityKind] = "ConvexColumnBuilder"; }; //#endregion //#region src/orm/builders/custom.ts function isRecord$1(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function isValidator(value) { return isRecord$1(value) && typeof value.kind === "string" && typeof value.isOptional === "string"; } function isColumnBuilder$1(value) { return isRecord$1(value) && value[entityKind] === "ColumnBuilder"; } function toRequiredValidator(validator) { return validator.isOptional === "optional" ? vRequired(validator) : validator; } function toRequiredBuilderValidator(validator) { const requiredValidator = toRequiredValidator(validator); if (requiredValidator.kind !== "union") return requiredValidator; const nonNullMembers = requiredValidator.members.filter((member) => member.kind !== "null"); if (nonNullMembers.length !== 1) return requiredValidator; const [member] = nonNullMembers; if (member.kind === "object" || member.kind === "array") return member; return requiredValidator; } function formatInvalidInput(path, value) { return `${path} expected a column builder, Convex validator, or nested object shape. Got ${Array.isArray(value) ? "array" : value === null ? "null" : typeof value}.`; } function objectShapeToValidator(shape, path) { const fields = {}; for (const [key, value] of Object.entries(shape)) fields[key] = nestedInputToValidator(value, `${path}.${key}`); return v.object(fields); } function nestedInputToValidator(input, path) { if (isColumnBuilder$1(input)) return toRequiredBuilderValidator(input.convexValidator); if (isValidator(input)) return toRequiredValidator(input); if (isRecord$1(input)) return objectShapeToValidator(input, path); throw new Error(formatInvalidInput(path, input)); } var ConvexCustomBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexCustomBuilder"; constructor(name, validator) { super(name, "any", "ConvexCustom"); this.config.validator = validator; } get convexValidator() { const validator = this.config.validator; if (this.config.notNull) return validator; return v.optional(v.union(v.null(), validator)); } build() { return this.convexValidator; } }; function custom(a, b) { if (b !== void 0) return new ConvexCustomBuilder(a, b); return new ConvexCustomBuilder("", a); } /** * Creates an array column from a nested validator or builder. * * Values in nested arrays are always compiled as required validators. */ function arrayOf(element) { return custom(v.array(nestedInputToValidator(element, "arrayOf(element)"))).$type(); } /** * Creates a union column from validators/builders without dropping to `v.union(...)`. */ function unionOf(...members) { const validators = members.map((member, index) => nestedInputToValidator(member, `unionOf(members[${index}])`)); return custom(v.union(...validators)).$type(); } /** * Creates an object column from either: * - a nested shape of validators/builders, or * - a validator/builder describing homogeneous record values * * Fields in nested objects are always compiled as required validators. */ function objectOf(input) { if (isColumnBuilder$1(input) || isValidator(input)) return custom(v.record(v.string(), nestedInputToValidator(input, "objectOf(value)"))).$type(); if (!isRecord$1(input)) throw new Error(formatInvalidInput("objectOf(shape)", input)); return custom(objectShapeToValidator(input, "objectOf(shape)")).$type(); } /** * Convenience wrapper for Convex "JSON" values. * * Note: This is Convex JSON (runtime `v.any()`), not SQL JSON/JSONB. */ function json() { return custom(v.any()).$type(); } //#endregion //#region src/orm/builders/id.ts /** * ID column builder class * Compiles to v.id(tableName) or v.optional(v.id(tableName)) */ var ConvexIdBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexIdBuilder"; constructor(name, tableName) { super(name, "string", "ConvexId"); this.tableName = tableName; this.config.referenceTable = tableName; } /** * Expose Convex validator for schema integration */ get convexValidator() { if (this.config.notNull) return v.id(this.tableName); return v.optional(v.union(v.null(), v.id(this.tableName))); } /** * Compile to Convex validator * .notNull() → v.id(tableName) * nullable → v.optional(v.id(tableName)) */ build() { return this.convexValidator; } }; function id(tableName) { return new ConvexIdBuilder("", tableName); } //#endregion //#region src/orm/builders/number.ts /** * Number column builder class * Compiles to v.number() or v.optional(v.number()) */ var ConvexNumberBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexNumberBuilder"; constructor(name) { super(name, "number", "ConvexNumber"); } /** * Expose Convex validator for schema integration */ get convexValidator() { if (this.config.notNull) return v.number(); return v.optional(v.union(v.null(), v.number())); } /** * Compile to Convex validator * .notNull() → v.number() * nullable → v.optional(v.number()) */ build() { return this.convexValidator; } }; function integer(name) { return new ConvexNumberBuilder(name ?? ""); } //#endregion //#region src/orm/builders/system-fields.ts /** * System Fields - Convex-provided fields available on all documents * * id: Document ID (string, backed by internal Convex _id) * createdAt: Creation timestamp alias (backed by internal Convex _creationTime) * * These are automatically added to every Convex table. */ var ConvexSystemIdBuilder = class extends ColumnBuilder { static [entityKind] = "ConvexSystemIdBuilder"; [entityKind] = "ConvexSystemIdBuilder"; constructor() { super("_id", "string", "ConvexSystemId"); this.config.notNull = true; } build() { return v.string(); } /** * Convex validator - runtime access * System fields use v.string() for _id */ get convexValidator() { return this.build(); } }; var ConvexSystemCreationTimeBuilder = class extends ColumnBuilder { static [entityKind] = "ConvexSystemCreationTimeBuilder"; [entityKind] = "ConvexSystemCreationTimeBuilder"; constructor() { super("_creationTime", "number", "ConvexSystemCreationTime"); this.config.notNull = true; } build() { return v.number(); } /** * Convex validator - runtime access * System fields use v.number() for _creationTime */ get convexValidator() { return this.build(); } }; var ConvexSystemCreatedAtBuilder = class extends ColumnBuilder { static [entityKind] = "ConvexSystemCreatedAtBuilder"; [entityKind] = "ConvexSystemCreatedAtBuilder"; constructor() { super("_creationTime", "number", "ConvexSystemCreatedAt"); this.config.notNull = true; } build() { return v.number(); } get convexValidator() { return this.build(); } }; function createSystemFields(tableName) { const id = new ConvexSystemIdBuilder(); const creationTime = new ConvexSystemCreationTimeBuilder(); const createdAt = new ConvexSystemCreatedAtBuilder(); id.config.tableName = tableName; creationTime.config.tableName = tableName; createdAt.config.tableName = tableName; return { id, _creationTime: creationTime, createdAt }; } //#endregion //#region src/orm/builders/text.ts /** * Text column builder class * Compiles to v.string() or v.optional(v.string()) */ var ConvexTextBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexTextBuilder"; constructor(name) { super(name, "string", "ConvexText"); } /** * Expose Convex validator for schema integration */ get convexValidator() { if (this.config.notNull) return v.string(); return v.optional(v.union(v.null(), v.string())); } /** * Compile to Convex validator * .notNull() → v.string() * nullable → v.optional(v.string()) */ build() { return this.convexValidator; } }; function text(name) { return new ConvexTextBuilder(name ?? ""); } //#endregion //#region src/orm/indexes.ts var ConvexIndexBuilderOn = class { static [entityKind] = "ConvexIndexBuilderOn"; [entityKind] = "ConvexIndexBuilderOn"; constructor(name, unique) { this.name = name; this.unique = unique; } on(...columns) { return new ConvexIndexBuilder(this.name, columns, this.unique); } }; var ConvexIndexBuilder = class { static [entityKind] = "ConvexIndexBuilder"; [entityKind] = "ConvexIndexBuilder"; config; constructor(name, columns, unique) { this.config = { name, columns, unique, where: void 0 }; } /** * Partial index conditions are not supported in Convex. * This method is kept for Drizzle API parity. */ where(condition) { this.config.where = condition; return this; } }; var ConvexSearchIndexBuilderOn = class { static [entityKind] = "ConvexSearchIndexBuilderOn"; [entityKind] = "ConvexSearchIndexBuilderOn"; constructor(name) { this.name = name; } on(searchField) { return new ConvexSearchIndexBuilder(this.name, searchField); } }; var ConvexSearchIndexBuilder = class { static [entityKind] = "ConvexSearchIndexBuilder"; [entityKind] = "ConvexSearchIndexBuilder"; config; constructor(name, searchField) { this.config = { name, searchField, filterFields: [], staged: false }; } filter(...fields) { this.config.filterFields = fields; return this; } staged() { this.config.staged = true; return this; } }; var ConvexVectorIndexBuilderOn = class { static [entityKind] = "ConvexVectorIndexBuilderOn"; [entityKind] = "ConvexVectorIndexBuilderOn"; constructor(name) { this.name = name; } on(vectorField) { return new ConvexVectorIndexBuilder(this.name, vectorField); } }; var ConvexAggregateIndexBuilderOn = class { static [entityKind] = "ConvexAggregateIndexBuilderOn"; [entityKind] = "ConvexAggregateIndexBuilderOn"; constructor(name) { this.name = name; } on(...columns) { return new ConvexAggregateIndexBuilder(this.name, columns); } all() { return new ConvexAggregateIndexBuilder(this.name, []); } }; var ConvexAggregateIndexBuilder = class { static [entityKind] = "ConvexAggregateIndexBuilder"; [entityKind] = "ConvexAggregateIndexBuilder"; config; constructor(name, columns) { this.config = { name, columns, countFields: [], sumFields: [], avgFields: [], minFields: [], maxFields: [] }; } count(...fields) { this.config.countFields = [...this.config.countFields, ...fields]; return this; } sum(...fields) { this.config.sumFields = [...this.config.sumFields, ...fields]; return this; } avg(...fields) { this.config.avgFields = [...this.config.avgFields, ...fields]; return this; } min(...fields) { this.config.minFields = [...this.config.minFields, ...fields]; return this; } max(...fields) { this.config.maxFields = [...this.config.maxFields, ...fields]; return this; } }; var ConvexRankIndexBuilderOn = class { static [entityKind] = "ConvexRankIndexBuilderOn"; [entityKind] = "ConvexRankIndexBuilderOn"; constructor(name) { this.name = name; } partitionBy(...columns) { return new ConvexRankIndexBuilder(this.name, columns, []); } all() { return new ConvexRankIndexBuilder(this.name, [], []); } }; var ConvexRankIndexBuilder = class { static [entityKind] = "ConvexRankIndexBuilder"; [entityKind] = "ConvexRankIndexBuilder"; config; constructor(name, partitionColumns, orderColumns) { this.config = { name, partitionColumns, orderColumns, sumField: void 0 }; } orderBy(...columns) { this.config.orderColumns = columns.map((entry) => { if (entry && typeof entry === "object" && "column" in entry && "direction" in entry) { const builder = entry.column?.builder; if (!builder) throw new Error("rankIndex orderBy() expected a column builder."); return { column: builder, direction: entry.direction }; } return { column: entry, direction: "asc" }; }); return this; } sum(field) { this.config.sumField = field; return this; } }; var ConvexVectorIndexBuilder = class { static [entityKind] = "ConvexVectorIndexBuilder"; [entityKind] = "ConvexVectorIndexBuilder"; config; constructor(name, vectorField) { this.config = { name, vectorField, dimensions: void 0, filterFields: [], staged: false }; } dimensions(dimensions) { if (!Number.isInteger(dimensions)) throw new Error(`Vector index '${this.config.name}' dimensions must be an integer, got ${dimensions}`); if (dimensions <= 0) throw new Error(`Vector index '${this.config.name}' dimensions must be positive, got ${dimensions}`); if (dimensions > 1e4) console.warn(`Vector index '${this.config.name}' has unusually large dimensions (${dimensions}). Common values: 768, 1536, 3072`); this.config.dimensions = dimensions; return this; } filter(...fields) { this.config.filterFields = fields; return this; } staged() { this.config.staged = true; return this; } }; function index(name) { return new ConvexIndexBuilderOn(name, false); } function uniqueIndex(name) { return new ConvexIndexBuilderOn(name, true); } function searchIndex(name) { return new ConvexSearchIndexBuilderOn(name); } function vectorIndex(name) { return new ConvexVectorIndexBuilderOn(name); } function aggregateIndex(name) { return new ConvexAggregateIndexBuilderOn(name); } function rankIndex(name) { return new ConvexRankIndexBuilderOn(name); } //#endregion //#region src/orm/rls/policies.ts var RlsPolicy = class { static [entityKind] = "RlsPolicy"; [entityKind] = "RlsPolicy"; as; for; to; using; withCheck; /** @internal */ _linkedTable; constructor(name, config) { this.name = name; if (config) { this.as = config.as; this.for = config.for; this.to = config.to; this.using = config.using; this.withCheck = config.withCheck; } } link(table) { this._linkedTable = table; return this; } }; function rlsPolicy(name, config) { return new RlsPolicy(name, config); } function isRlsPolicy(value) { return !!value && typeof value === "object" && value[entityKind] === "RlsPolicy"; } //#endregion //#region src/orm/symbols.ts const TableName = Symbol.for("kitcn:TableName"); const Columns = Symbol.for("kitcn:Columns"); const Brand = Symbol.for("kitcn:Brand"); const Relations = Symbol.for("kitcn:Relations"); const OrmContext = Symbol.for("kitcn:OrmContext"); const RlsPolicies = Symbol.for("kitcn:RlsPolicies"); const EnableRLS = Symbol.for("kitcn:EnableRLS"); const TableDeleteConfig = Symbol.for("kitcn:TableDeleteConfig"); const TablePolymorphic = Symbol.for("kitcn:TablePolymorphic"); const OrmSchemaOptions = Symbol.for("kitcn:OrmSchemaOptions"); const OrmSchemaDefinition = Symbol.for("kitcn:OrmSchemaDefinition"); const OrmSchemaExtensionTables = Symbol.for("kitcn:OrmSchemaExtensionTables"); const OrmSchemaExtensions = Symbol.for("kitcn:OrmSchemaExtensions"); const OrmSchemaExtensionRelations = Symbol.for("kitcn:OrmSchemaExtensionRelations"); const OrmSchemaExtensionTriggers = Symbol.for("kitcn:OrmSchemaExtensionTriggers"); const OrmSchemaRelations = Symbol.for("kitcn:OrmSchemaRelations"); const OrmSchemaTriggers = Symbol.for("kitcn:OrmSchemaTriggers"); //#endregion //#region src/orm/table.ts /** * Reserved Convex system table names that cannot be used */ const RESERVED_TABLES = new Set(["_storage", "_scheduled_functions"]); const RESERVED_COLUMN_NAMES = new Set([ "id", "_id", "_creationTime" ]); const DEFAULT_POLYMORPHIC_ALIAS = "details"; const CONVEX_TABLE_FIELD_LIMIT = 1024; /** * Valid table name pattern: starts with letter/underscore, contains only alphanumeric and underscore */ const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/; /** * Validate table name against Convex constraints */ function validateTableName(name) { if (RESERVED_TABLES.has(name)) throw new Error(`Table name '${name}' is reserved. System tables cannot be redefined.`); if (!TABLE_NAME_REGEX.test(name)) throw new Error(`Invalid table name '${name}'. Must start with letter, contain only alphanumeric and underscore.`); } /** * Create a Convex object validator from column builders * * Extracts .convexValidator from each column and creates v.object({...}) * This is the core factory that bridges ORM columns to Convex validators. * * @param columns - Record of column name to column builder * @returns Convex object validator */ function createValidatorFromColumns(columns) { const validatorFields = Object.fromEntries(Object.entries(columns).map(([key, builder]) => [key, builder.convexValidator])); return v.object(validatorFields); } function discriminator(config) { if (!config || typeof config !== "object") throw new Error("discriminator(...) requires a config object."); if (!config.variants || typeof config.variants !== "object" || Object.keys(config.variants).length === 0) throw new Error("discriminator(...).variants must contain at least one case."); if (config.as !== void 0 && (typeof config.as !== "string" || config.as.length === 0)) throw new Error("discriminator(...).as must be a non-empty string when set."); const builder = text().notNull(); builder.__polymorphic = { as: config.as ?? DEFAULT_POLYMORPHIC_ALIAS, variants: config.variants }; builder.config.discriminator = { as: config.as, variants: config.variants }; return builder; } var ConvexDeletionBuilder = class { static [entityKind] = "ConvexDeletionBuilder"; [entityKind] = "ConvexDeletionBuilder"; constructor(config) { this.config = config; } }; function deletion(mode, options) { if (options?.delayMs !== void 0) { if (mode !== "scheduled") throw new Error("deletion() delayMs is only supported for 'scheduled'."); if (!Number.isInteger(options.delayMs) || options.delayMs < 0) throw new Error("deletion() delayMs must be a non-negative integer when mode is 'scheduled'."); } return new ConvexDeletionBuilder({ mode, delayMs: options?.delayMs }); } function isConvexIndexBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexIndexBuilder"; } function isConvexIndexBuilderOn(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexIndexBuilderOn"; } function isConvexAggregateIndexBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexAggregateIndexBuilder"; } function isConvexAggregateIndexBuilderOn(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexAggregateIndexBuilderOn"; } function isConvexRankIndexBuilderOn(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexRankIndexBuilderOn"; } function isConvexRankIndexBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexRankIndexBuilder"; } function isConvexUniqueConstraintBuilderOn(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexUniqueConstraintBuilderOn"; } function isConvexForeignKeyBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexForeignKeyBuilder"; } function isConvexCheckBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexCheckBuilder"; } function isConvexSearchIndexBuilderOn(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexSearchIndexBuilderOn"; } function isConvexUniqueConstraintBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexUniqueConstraintBuilder"; } function isConvexSearchIndexBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexSearchIndexBuilder"; } function isConvexVectorIndexBuilderOn(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexVectorIndexBuilderOn"; } function isConvexVectorIndexBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexVectorIndexBuilder"; } function isConvexDeletionBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexDeletionBuilder"; } function isConvexLifecycleBuilder(value) { return typeof value === "object" && value !== null && value[entityKind] === "ConvexLifecycleBuilder"; } function getColumnName(column) { const config = column.config; if (!config?.name) throw new Error("Invalid index column: expected a convexTable column builder."); return config.name; } function getColumnType(column) { return column.config?.columnType; } function getColumnDimensions(column) { return column.config?.dimensions; } function getColumnTableName(column) { const config = column.config; return config?.tableName ?? config?.referenceTable; } function getColumnTable(column) { return column.config?.table; } function getUniqueIndexName(tableName, fields, explicitName) { if (explicitName) return explicitName; return `${tableName}_${fields.join("_")}_unique`; } function assertColumnInTable(column, expectedTable, context) { const tableName = getColumnTableName(column); if (tableName && tableName !== expectedTable) throw new Error(`${context} references column from '${tableName}', but belongs to '${expectedTable}'.`); return getColumnName(column); } function assertNoReservedCreatedAtIndexFields(fields, context) { if (fields.includes("createdAt")) throw new Error(`${context} cannot use 'createdAt'. 'createdAt' is reserved and maps to internal '_creationTime'.`); } function assertSearchFieldType(column, indexName) { const columnType = getColumnType(column) ?? "unknown"; if (columnType !== "ConvexText") throw new Error(`Search index '${indexName}' only supports text() columns. Field '${getColumnName(column)}' is type '${columnType}'.`); } function assertVectorFieldType(column, indexName) { const columnType = getColumnType(column) ?? "unknown"; if (columnType !== "ConvexVector") throw new Error(`Vector index '${indexName}' requires a vector() column. Field '${getColumnName(column)}' is type '${columnType}'.`); } function assertAggregateSumFieldType(column, indexName) { const columnType = getColumnType(column) ?? "unknown"; if (!["ConvexNumber", "ConvexTimestamp"].includes(columnType)) throw new Error(`aggregateIndex '${indexName}' sum() supports integer()/timestamp() columns only. Field '${getColumnName(column)}' is type '${columnType}'.`); } function assertAggregateAvgFieldType(column, indexName) { const columnType = getColumnType(column) ?? "unknown"; if (!["ConvexNumber", "ConvexTimestamp"].includes(columnType)) throw new Error(`aggregateIndex '${indexName}' avg() supports integer()/timestamp() columns only. Field '${getColumnName(column)}' is type '${columnType}'.`); } function assertAggregateComparableFieldType(column, indexName, method) { const columnType = getColumnType(column) ?? "unknown"; if (![ "ConvexNumber", "ConvexTimestamp", "ConvexDate", "ConvexText", "ConvexBoolean", "ConvexId" ].includes(columnType)) throw new Error(`aggregateIndex '${indexName}' ${method}() does not support column type '${columnType}' on '${getColumnName(column)}'.`); } function assertRankOrderFieldType(column, indexName) { const columnType = getColumnType(column) ?? "unknown"; if (![ "ConvexNumber", "ConvexTimestamp", "ConvexDate" ].includes(columnType)) throw new Error(`rankIndex '${indexName}' orderBy() supports integer()/timestamp()/date() columns only. Field '${getColumnName(column)}' is type '${columnType}'.`); } const dedupeFieldNames = (fields) => [...new Set(fields)]; const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value); const isColumnBuilder = (value) => isRecord(value) && typeof value.build === "function"; const getDiscriminatorConfig = (value) => { if (!isColumnBuilder(value)) return; const discriminator = value.config?.discriminator; if (!discriminator) return; return discriminator; }; const getPolymorphicFieldSignature = (column) => { const validator = column.convexValidator ?? column.build(); return JSON.stringify({ columnType: column.config?.columnType, validator: validator?.json }); }; function resolveTableColumns(tableName, columns) { const resolvedColumns = {}; const pendingPolymorphic = []; for (const [columnName, rawBuilder] of Object.entries(columns)) { if (!isColumnBuilder(rawBuilder)) throw new Error(`Column '${columnName}' on '${tableName}' must be a column builder.`); resolvedColumns[columnName] = rawBuilder; const discriminatorConfig = getDiscriminatorConfig(rawBuilder); if (!discriminatorConfig) continue; if (!isRecord(discriminatorConfig.variants) || Object.keys(discriminatorConfig.variants).length === 0) throw new Error(`discriminator('${tableName}.${columnName}') requires at least one variant.`); const alias = discriminatorConfig.as === void 0 ? DEFAULT_POLYMORPHIC_ALIAS : discriminatorConfig.as; if (typeof alias !== "string" || alias.length === 0) throw new Error(`discriminator('${tableName}.${columnName}').as must be a non-empty string.`); pendingPolymorphic.push({ discriminator: columnName, alias, variants: discriminatorConfig.variants }); } if (pendingPolymorphic.length > 1) throw new Error(`Only one discriminator(...) column is currently supported on '${tableName}'.`); const polymorphicConfigs = []; for (const pending of pendingPolymorphic) { if (pending.alias in resolvedColumns) throw new Error(`discriminator('${tableName}.${pending.discriminator}') alias '${pending.alias}' collides with an existing column.`); const generatedFieldMap = /* @__PURE__ */ new Map(); const variantRuntime = {}; for (const [variantKey, rawVariantColumns] of Object.entries(pending.variants)) { if (!isRecord(rawVariantColumns)) throw new Error(`discriminator('${tableName}.${pending.discriminator}') variant '${variantKey}' must be an object.`); const fieldNames = []; const requiredFieldNames = []; for (const [fieldName, rawFieldBuilder] of Object.entries(rawVariantColumns)) { if (!isColumnBuilder(rawFieldBuilder)) throw new Error(`discriminator('${tableName}.${pending.discriminator}').variants.${variantKey}.${fieldName} must be a column builder.`); if (fieldName in resolvedColumns) throw new Error(`discriminator('${tableName}.${pending.discriminator}').variants.${variantKey}.${fieldName} collides with an existing table column.`); const fieldBuilder = rawFieldBuilder; const fieldConfig = fieldBuilder.config; const isRequiredForVariant = fieldConfig?.notNull === true && fieldConfig.hasDefault !== true && typeof fieldConfig.defaultFn !== "function"; const signature = getPolymorphicFieldSignature(fieldBuilder); const existing = generatedFieldMap.get(fieldName); if (existing && existing.signature !== signature) throw new Error(`discriminator('${tableName}.${pending.discriminator}') field '${fieldName}' has conflicting builder signatures across variants.`); if (!existing) { if (fieldConfig) fieldConfig.notNull = false; generatedFieldMap.set(fieldName, { builder: fieldBuilder, signature }); } fieldNames.push(fieldName); if (isRequiredForVariant) requiredFieldNames.push(fieldName); } variantRuntime[variantKey] = { fieldNames, requiredFieldNames }; } for (const [fieldName, { builder }] of generatedFieldMap.entries()) resolvedColumns[fieldName] = builder; polymorphicConfigs.push({ discriminator: pending.discriminator, alias: pending.alias, generatedFieldNames: Object.freeze([...generatedFieldMap.keys()]), variants: Object.freeze(variantRuntime) }); } if (Object.keys(resolvedColumns).length > CONVEX_TABLE_FIELD_LIMIT) throw new Error(`Table '${tableName}' exceeds Convex field count limit (${CONVEX_TABLE_FIELD_LIMIT}) after discriminator expansion.`); return { columns: resolvedColumns, polymorphicConfigs }; } function applyExtraConfig(table, config) { if (!config) return; const entries = Array.isArray(config) ? config : Object.values(config); for (const entry of entries) { if (isConvexIndexBuilderOn(entry)) throw new Error(`Invalid index definition on '${table.tableName}'. Did you forget to call .on(...)?`); if (isConvexUniqueConstraintBuilderOn(entry)) throw new Error(`Invalid unique constraint definition on '${table.tableName}'. Did you forget to call .on(...)?`); if (isConvexAggregateIndexBuilderOn(entry)) throw new Error(`Invalid aggregate index definition on '${table.tableName}'. Did you forget to call .on(...) or .all()?`); if (isConvexRankIndexBuilderOn(entry)) throw new Error(`Invalid rank index definition on '${table.tableName}'. Did you forget to call .partitionBy(...) or .all()?`); if (isConvexSearchIndexBuilderOn(entry)) throw new Error(`Invalid search index definition on '${table.tableName}'. Did you forget to call .on(...)?`); if (isConvexVectorIndexBuilderOn(entry)) throw new Error(`Invalid vector index definition on '${table.tableName}'. Did you forget to call .on(...)?`); if (isRlsPolicy(entry)) { const target = entry._linkedTable ?? table; if (typeof target.addRlsPolicy === "function") target.addRlsPolicy(entry); else { const policies = target[RlsPolicies] ?? []; policies.push(entry); target[RlsPolicies] = policies; target[EnableRLS] = true; } continue; } if (isConvexDeletionBuilder(entry)) { if (table[TableDeleteConfig]) throw new Error(`Only one deletion(...) config can be defined for '${table.tableName}'.`); table[TableDeleteConfig] = { mode: entry.config.mode, delayMs: entry.config.delayMs }; continue; } if (isConvexLifecycleBuilder(entry)) throw new Error(`Lifecycle hooks are no longer supported inside convexTable('${table.tableName}', ..., extraConfig). Export schema triggers with defineTriggers(relations, { ... }) from schema.ts.`); if (isConvexIndexBuilder(entry)) { const { name, columns, unique, where } = entry.config; if (where) throw new Error(`Convex does not support partial indexes. Remove .where(...) from index '${name}'.`); if (unique) {} const fields = columns.map((column) => assertColumnInTable(column, table.tableName, `Index '${name}'`)); assertNoReservedCreatedAtIndexFields(fields, `Index '${name}'`); table.addIndex(name, fields); if (unique) table.addUniqueIndex(name, fields, false); continue; } if (isConvexAggregateIndexBuilder(entry)) { const { name, columns, countFields, sumFields, avgFields, minFields, maxFields } = entry.config; const fields = columns.map((column) => assertColumnInTable(column, table.tableName, `Aggregate index '${name}'`)); assertNoReservedCreatedAtIndexFields(fields, `Aggregate index '${name}'`); const resolvedCountFields = dedupeFieldNames(countFields.map((column) => assertColumnInTable(column, table.tableName, `Aggregate index '${name}' count`))); assertNoReservedCreatedAtIndexFields(resolvedCountFields, `Aggregate index '${name}' count`); const resolvedSumFields = dedupeFieldNames(sumFields.map((column) => { const field = assertColumnInTable(column, table.tableName, `Aggregate index '${name}' sum`); assertAggregateSumFieldType(column, name); return field; })); assertNoReservedCreatedAtIndexFields(resolvedSumFields, `Aggregate index '${name}' sum`); const resolvedAvgFields = dedupeFieldNames(avgFields.map((column) => { const field = assertColumnInTable(column, table.tableName, `Aggregate index '${name}' avg`); assertAggregateAvgFieldType(column, name); return field; })); assertNoReservedCreatedAtIndexFields(resolvedAvgFields, `Aggregate index '${name}' avg`); const resolvedMinFields = dedupeFieldNames(minFields.map((column) => { const field = assertColumnInTable(column, table.tableName, `Aggregate index '${name}' min`); assertAggregateComparableFieldType(column, name, "min"); return field; })); assertNoReservedCreatedAtIndexFields(resolvedMinFields, `Aggregate index '${name}' min`); const resolvedMaxFields = dedupeFieldNames(maxFields.map((column) => { const field = assertColumnInTable(column, table.tableName, `Aggregate index '${name}' max`); assertAggregateComparableFieldType(column, name, "max"); return field; })); assertNoReservedCreatedAtIndexFields(resolvedMaxFields, `Aggregate index '${name}' max`); table.addAggregateIndex(name, { fields, countFields: resolvedCountFields, sumFields: resolvedSumFields, avgFields: resolvedAvgFields, minFields: resolvedMinFields, maxFields: resolvedMaxFields }); continue; } if (isConvexRankIndexBuilder(entry)) { const { name, partitionColumns, orderColumns, sumField } = entry.config; if (!orderColumns.length) throw new Error(`rankIndex '${name}' on '${table.tableName}' must declare at least one orderBy(...) column.`); const resolvedPartitionFields = dedupeFieldNames(partitionColumns.map((column) => assertColumnInTable(column, table.tableName, `rankIndex '${name}'`))); assertNoReservedCreatedAtIndexFields(resolvedPartitionFields, `rankIndex '${name}' partitionBy`); const resolvedOrderFields = orderColumns.map((entry) => { const field = assertColumnInTable(entry.column, table.tableName, `rankIndex '${name}' orderBy`); assertNoReservedCreatedAtIndexFields([field], `rankIndex '${name}'`); assertRankOrderFieldType(entry.column, name); return { field, direction: entry.direction }; }); const resolvedSumField = sumField ? (() => { const field = assertColumnInTable(sumField, table.tableName, `rankIndex '${name}' sum`); assertNoReservedCreatedAtIndexFields([field], `rankIndex '${name}'`); assertAggregateSumFieldType(sumField, name); return field; })() : void 0; table.addRankIndex(name, { partitionFields: resolvedPartitionFields, orderFields: resolvedOrderFields, sumField: resolvedSumField }); continue; } if (isConvexUniqueConstraintBuilder(entry)) { const { name, columns, nullsNotDistinct } = entry.config; const fields = columns.map((column) => assertColumnInTable(column, table.tableName, "Unique constraint")); assertNoReservedCreatedAtIndexFields(fields, "Unique constraint"); const indexName = getUniqueIndexName(table.tableName, fields, name); table.addIndex(indexName, fields); table.addUniqueIndex(indexName, fields, nullsNotDistinct); continue; } if (isConvexForeignKeyBuilder(entry)) { const { name, columns, foreignColumns, onDelete, onUpdate } = entry.config; if (columns.length === 0 || foreignColumns.length === 0) throw new Error(`Foreign key on '${table.tableName}' requires at least one column.`); if (columns.length !== foreignColumns.length) throw new Error(`Foreign key on '${table.tableName}' must specify matching columns and foreignColumns.`); const localFields = columns.map((column) => assertColumnInTable(column, table.tableName, "Foreign key")); const foreignTableName = getColumnTableName(foreignColumns[0]); if (!foreignTableName) throw new Error(`Foreign key on '${table.tableName}' references a column without a table.`); const foreignTable = getColumnTable(foreignColumns[0]); const foreignFields = foreignColumns.map((column) => { const tableName = getColumnTableName(column); if (tableName && tableName !== foreignTableName) throw new Error(`Foreign key on '${table.tableName}' mixes foreign columns from '${foreignTableName}' and '${tableName}'.`); return getColumnName(column); }); table.addForeignKey({ name, columns: localFields, foreignTableName, foreignTable, foreignColumns: foreignFields, onDelete, onUpdate }); continue; } if (isConvexCheckBuilder(entry)) { const { name, expression } = entry.config; table.addCheck(name, expression); continue; } if (isConvexSearchIndexBuilder(entry)) { const { name, searchField, filterFields, staged } = entry.config; const searchFieldName = assertColumnInTable(searchField, table.tableName, `Search index '${name}'`); assertNoReservedCreatedAtIndexFields([searchFieldName], `Search index '${name}'`); assertSearchFieldType(searchField, name); const filterFieldNames = filterFields.map((field) => assertColumnInTable(field, table.tableName, `Search index '${name}'`)); assertNoReservedCreatedAtIndexFields(filterFieldNames, `Search index '${name}'`); table.addSearchIndex(name, { searchField: searchFieldName, filterFields: filterFieldNames, staged }); continue; } if (isConvexVectorIndexBuilder(entry)) { const { name, vectorField, dimensions, filterFields, staged } = entry.config; if (dimensions === void 0) throw new Error(`Vector index '${name}' is missing dimensions. Call .dimensions(n) before using.`); const vectorFieldName = assertColumnInTable(vectorField, table.tableName, `Vector index '${name}'`); assertNoReservedCreatedAtIndexFields([vectorFieldName], `Vector index '${name}'`); assertVectorFieldType(vectorField, name); const columnDimensions = getColumnDimensions(vectorField); if (columnDimensions !== void 0 && columnDimensions !== dimensions) throw new Error(`Vector index '${name}' dimensions (${dimensions}) do not match vector column '${vectorFieldName}' dimensions (${columnDimensions}).`); const filterFieldNames = filterFields.map((field) => assertColumnInTable(field, table.tableName, `Vector index '${name}'`)); assertNoReservedCreatedAtIndexFields(filterFieldNames, `Vector index '${name}'`); table.addVectorIndex(name, { vectorField: vectorFieldName, dimensions, filterFields: filterFieldNames, staged }); continue; } throw new Error(`Unsupported extra config value in convexTable('${table.tableName}').`); } } /** * ConvexTable implementation class * Provides all properties required by Convex's TableDefinition * * Following convex-ents pattern: * - Private fields for indexes (matches TableDefinition structure) * - Duck typing (defineSchema only checks object shape) * - Direct validator storage (no re-wrapping) */ var ConvexTableImpl = class { /** * Required by TableDefinition * Public validator property containing v.object({...}) with all column validators */ validator; /** * TableDefinition private fields * These satisfy structural typing requirements for defineSchema() */ indexes = []; uniqueIndexes = []; aggregateIndexes = []; rankIndexes = []; foreignKeys = []; deferredForeignKeys = []; deferredForeignKeysResolved = false; stagedDbIndexes = []; searchIndexes = []; stagedSearchIndexes = []; vectorIndexes = []; stagedVectorIndexes = []; checks = []; /** * Symbol-based metadata storage */ [TableName]; [Columns]; [Brand] = "ConvexTable"; [EnableRLS] = false; [RlsPolicies] = []; [TableDeleteConfig]; [TablePolymorphic]; /** * Public tableName for convenience */ tableName; constructor(name, columns, polymorphicConfigs) { validateTableName(name); for (const columnName of Object.keys(columns)) if (RESERVED_COLUMN_NAMES.has(columnName)) throw new Error(`Column name '${columnName}' is reserved. System fields are managed by Convex ORM.`); this[TableName] = name; const namedColumns = Object.fromEntries(Object.entries(columns).map(([columnName, builder]) => { builder.config.name = columnName; builder.config.tableName = name; builder.config.table = this; return [columnName, builder]; })); this[Columns] = namedColumns; this.tableName = name; if (polymorphicConfigs && polymorphicConfigs.length > 0) this[TablePolymorphic] = polymorphicConfigs; this.validator = createValidatorFromColumns(namedColumns); for (const [columnName, builder] of Object.entries(namedColumns)) { const config = builder.config; if (config?.isUnique) { const indexName = getUniqueIndexName(name, [columnName], config.uniqueName); const nullsNotDistinct = config.uniqueNulls === "not distinct"; this.addIndex(indexName, [columnName]); this.addUniqueIndex(indexName, [columnName], nullsNotDistinct); } if (config?.referenceTable && (!config.foreignKeyConfigs || config.foreignKeyConfigs.length === 0)) this.addForeignKey({ name: void 0, columns: [columnName], foreignTableName: config.referenceTable, foreignColumns: ["_id"] }); if (config?.foreignKeyConfigs?.length) for (const foreignConfig of config.foreignKeyConfigs) this.deferredForeignKeys.push({ localColumnName: columnName, ref: foreignConfig.ref, config: foreignConfig.config }); } } getPolymorphicConfigs() { return this[TablePolymorphic]; } /** * Internal: add index to table from builder extraConfig * */ addIndex(name, fields) { this.indexes.push({ indexDescriptor: name, fields }); } /** * Internal: add unique index metadata for runtime enforcement */ addUniqueIndex(name, fields, nullsNotDistinct) { this.uniqueIndexes.push({ name, fields, nullsNotDistinct }); } addAggregateIndex(name, config) { if (this.aggregateIndexes.some((index) => index.name === name) || this.rankIndexes.some((index) => index.name === name)) throw new Error(`Duplicate aggregate index '${name}' on '${this.tableName}'.`); this.aggregateIndexes.push({ name, fields: config.fields, countFields: config.countFields, sumFields: config.sumFields, avgFields: config.avgFields, minFields: config.minFields, maxFields: config.maxFields }); } addRankIndex(name, config) { if (this.rankIndexes.some((index) => index.name === name) || this.aggregateIndexes.some((index) => index.name === name)) throw new Error(`Duplicate aggregate index '${name}' on '${this.tableName}'.`); this.rankIndexes.push({ name, partitionFields: config.partitionFields, orderFields: config.orderFields, sumField: config.sumField }); } getAggregateIndexes() { return [...this.aggregateIndexes]; } getRankIndexes() { return [...this.rankIndexes]; } /** * Internal: expose unique index metadata for mutation enforcement */ getUniqueIndexes() { return this.uniqueIndexes; } /** * Internal: expose index metadata for runtime enforcement */ getIndexes() { return this.indexes.map((entry) => ({ name: entry.indexDescriptor, fields: entry.fields })); } /** * Internal: expose search index metadata for runtime query execution */ getSearchIndexes() { return this.searchIndexes.map((entry) => ({ name: entry.indexDescriptor, searchField: entry.searchField, filterFields: entry.filterFields })); } /** * Internal: expose vector index metadata for runtime query execution */ getVectorIndexes() { return this.vectorIndexes.map((entry) => ({ name: entry.indexDescriptor, vectorField: entry.vectorField, dimensions: entry.dimensions, filterFields: entry.filterFields })); } /** * Internal: attach an RLS policy to this table */ addRlsPolicy(policy) { this[RlsPolicies].push(policy); this[EnableRLS] = true; } /** * Internal: return attached RLS policies */ getRlsPolicies() { return this[RlsPolicies]; } /** * Internal: check if RLS is enabled on this table */ isRlsEnabled() { return this[EnableRLS]; } /** * Internal: add foreign key metadata for runtime enforcement */ addForeignKey(definition) { const matches = (existing) => { if (existing.foreignTableName !== definition.foreignTableName) return false; if (existing.columns.length !== definition.columns.length) return false; if (existing.foreignColumns.length !== definition.foreignColumns.length) return false; for (let i = 0; i < existing.columns.length; i++) if (existing.columns[i] !== definition.columns[i]) return false; for (let i = 0; i < existing.foreignColumns.length; i++) if (existing.foreignColumns[i] !== definition.foreignColumns[i]) return false; return true; }; this.foreignKeys = this.foreignKeys.filter((existing) => !matches(existing)); this.foreignKeys.push(definition); } resolveDeferredForeignKeys() { if (this.deferredForeignKeysResolved) return; this.deferredForeignKeysResolved = true; for (const deferred of this.deferredForeignKeys) { let foreignColumn; try { foreignColumn = deferred.ref(); } catch (error) { const reason = error instanceof Error ? ` ${error.message}` : ""; throw new Error(`Failed to resolve foreign key reference for '${this.tableName}.${deferred.localColumnName}'. Use references(() => targetTable.column) after both tables are declared.${reason}`); } const foreignTableName = getColumnTableName(foreignColumn); if (!foreignTableName) throw new Error(`Foreign key on '${this.tableName}.${deferred.localColumnName}' references a column without a table. Use references(() => targetTable.column).`); const foreignTable = getColumnTable(foreignColumn); if (!foreignTable) throw new Error(`Foreign key on '${this.tableName}.${deferred.localColumnName}' references a column without table metadata. Replace references(() => id('tableName')) with references(() => table.id).`); const foreignColumnName = getColumnName(foreignColumn); this.addForeignKey({ name: deferred.config.name, columns: [deferred.localColumnName], foreignTableName, foreignTable, foreignColumns: [foreignColumnName], onDelete: deferred.config.onDelete, onUpdate: deferred.config.onUpdate }); } this.deferredForeignKeys = []; } /** * Internal: expose foreign key metadata for mutation enforcement */ getForeignKeys() { this.resolveDeferredForeignKeys(); return this.foreignKeys; } addCheck(name, expression) { this.checks.push({ name, expression }); } getChecks() { return this.checks; } /** * Internal: add search index to table from builder extraConfig */ addSearchIndex(name, config) { const entry = { indexDescriptor: name, searchField: config.searchField, filterFields: config.filterF