UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

1,143 lines (1,132 loc) 460 kB
import { A as vectorIndex, B as ConvexColumnBuilder, C as RlsPolicy, D as rankIndex, E as index, F as arrayOf, I as custom, L as json, M as createSystemFields, N as integer, O as searchIndex, P as id, R as objectOf, S as TablePolymorphic, T as aggregateIndex, V as entityKind, _ as OrmSchemaRelations, a as deletion, b as TableDeleteConfig, c as Columns, d as OrmSchemaDefinition, f as OrmSchemaExtensionRelations, g as OrmSchemaOptions, h as OrmSchemaExtensions, i as convexTable, j as text, k as uniqueIndex, l as EnableRLS, m as OrmSchemaExtensionTriggers, o as discriminator, p as OrmSchemaExtensionTables, s as Brand, t as DirectAggregate, u as OrmContext, v as OrmSchemaTriggers, w as rlsPolicy, x as TableName, y as RlsPolicies, z as unionOf } from "../runtime-i6t-HoZn.js"; import { a as pretendRequired, i as pretend, n as deprecated } from "../validators-C7LelqTN.js"; import { A as ne, C as inArray, D as like, E as isNull, F as notLike, I as or, L as startsWith, M as notBetween, N as notIlike, O as lt, P as notInArray, S as ilike, T as isNotNull, _ as endsWith, a as mergedStream, b as gt, c as isUnsetToken, d as arrayContained, f as arrayContains, g as contains, h as column, i as getIndexFields, j as not, k as lte, l as unsetToken, m as between, n as EmptyStream, o as stream, p as arrayOverlaps, r as QueryStream, s as streamIndexRange, t as getByIdWithOrmQueryFallback, u as and, v as eq, w as isFieldReference, x as gte, y as fieldRef } from "../query-context-ydn9kb6P.js"; import { v } from "convex/values"; import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric } from "convex/server"; //#region src/orm/builders/bigint.ts /** * BigInt column builder class * Compiles to v.int64() or v.optional(v.int64()) */ var ConvexBigIntBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexBigIntBuilder"; constructor(name) { super(name, "bigint", "ConvexBigInt"); } /** * Expose Convex validator for schema integration */ get convexValidator() { if (this.config.notNull) return v.int64(); return v.optional(v.union(v.null(), v.int64())); } /** * Compile to Convex validator * .notNull() → v.int64() * nullable → v.optional(v.int64()) */ build() { return this.convexValidator; } }; function bigint(name) { return new ConvexBigIntBuilder(name ?? ""); } //#endregion //#region src/orm/builders/boolean.ts /** * Boolean column builder class * Compiles to v.boolean() or v.optional(v.boolean()) */ var ConvexBooleanBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexBooleanBuilder"; constructor(name) { super(name, "boolean", "ConvexBoolean"); } /** * Expose Convex validator for schema integration */ get convexValidator() { if (this.config.notNull) return v.boolean(); return v.optional(v.union(v.null(), v.boolean())); } /** * Compile to Convex validator * .notNull() → v.boolean() * nullable → v.optional(v.boolean()) */ build() { return this.convexValidator; } }; function boolean(name) { return new ConvexBooleanBuilder(name ?? ""); } //#endregion //#region src/orm/builders/bytes.ts var ConvexBytesBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexBytesBuilder"; constructor(name) { super(name, "bytes", "ConvexBytes"); } get convexValidator() { if (this.config.notNull) return v.bytes(); return v.optional(v.union(v.null(), v.bytes())); } build() { return this.convexValidator; } }; function bytes(name) { return new ConvexBytesBuilder(name ?? ""); } //#endregion //#region src/orm/builders/date.ts const toDateOnlyIsoString = (value) => value.toISOString().slice(0, 10); var ConvexDateBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexDateBuilder"; constructor(name, mode) { super(name, "string", "ConvexDate"); this.config.mode = mode; } get convexValidator() { if (this.config.notNull) return v.string(); return v.optional(v.union(v.null(), v.string())); } defaultNow() { if (this.config.mode === "date") return this.$defaultFn(() => /* @__PURE__ */ new Date()); return this.$defaultFn(() => toDateOnlyIsoString(/* @__PURE__ */ new Date())); } build() { return this.convexValidator; } }; const normalizeDateFactoryArgs = (nameOrConfig, maybeConfig) => { return { name: typeof nameOrConfig === "string" ? nameOrConfig : "", mode: (typeof nameOrConfig === "string" ? maybeConfig : nameOrConfig)?.mode ?? "string" }; }; function date(nameOrConfig, maybeConfig) { const { name, mode } = normalizeDateFactoryArgs(nameOrConfig, maybeConfig); return new ConvexDateBuilder(name, mode); } //#endregion //#region src/orm/builders/text-enum.ts var ConvexTextEnumBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexTextEnumBuilder"; constructor(name, values) { super(name, "string", "ConvexText"); this.config.values = [...values]; } _enumValidator() { const literals = this.config.values.map((value) => v.literal(value)); if (literals.length === 1) return literals[0]; return v.union(...literals); } get convexValidator() { const base = this._enumValidator(); if (this.config.notNull) return base; return v.optional(v.union(v.null(), base)); } build() { return this.convexValidator; } }; function textEnum(values) { return new ConvexTextEnumBuilder("", values); } //#endregion //#region src/orm/builders/timestamp.ts var ConvexTimestampBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexTimestampBuilder"; constructor(name, mode) { super(name, "number", "ConvexTimestamp"); this.config.mode = mode; } get convexValidator() { if (this.config.notNull && this.config.name === "createdAt" && typeof this.config.defaultFn === "function") return v.optional(v.number()); if (this.config.notNull) return v.number(); return v.optional(v.union(v.null(), v.number())); } defaultNow() { if (this.config.mode === "string") return this.$defaultFn(() => (/* @__PURE__ */ new Date()).toISOString()); return this.$defaultFn(() => /* @__PURE__ */ new Date()); } build() { return this.convexValidator; } }; const normalizeTimestampFactoryArgs = (nameOrConfig, maybeConfig) => { return { name: typeof nameOrConfig === "string" ? nameOrConfig : "", mode: (typeof nameOrConfig === "string" ? maybeConfig : nameOrConfig)?.mode ?? "date" }; }; function timestamp(nameOrConfig, maybeConfig) { const { name, mode } = normalizeTimestampFactoryArgs(nameOrConfig, maybeConfig); return new ConvexTimestampBuilder(name, mode); } //#endregion //#region src/orm/builders/vector.ts const MAX_VECTOR_DIMENSIONS = 1e4; function validateVectorDimensions(dimensions, columnName) { if (!Number.isInteger(dimensions)) throw new Error(`Vector column '${columnName}' dimensions must be an integer, got ${dimensions}`); if (dimensions <= 0) throw new Error(`Vector column '${columnName}' dimensions must be positive, got ${dimensions}`); if (dimensions > MAX_VECTOR_DIMENSIONS) console.warn(`Vector column '${columnName}' has unusually large dimensions (${dimensions}). Common values: 768, 1536, 3072`); } /** * Vector column builder class * Compiles to v.array(v.float64()) or v.optional(v.array(v.float64())) */ var ConvexVectorBuilder = class extends ConvexColumnBuilder { static [entityKind] = "ConvexVectorBuilder"; constructor(name, dimensions) { super(name, "vector", "ConvexVector"); validateVectorDimensions(dimensions, name || "vector"); this.config.dimensions = dimensions; } get dimensions() { return this.config.dimensions; } /** * Expose Convex validator for schema integration */ get convexValidator() { const validator = v.array(v.float64()); if (this.config.notNull) return validator; return v.optional(v.union(v.null(), validator)); } /** * Compile to Convex validator * .notNull() → v.array(v.float64()) * nullable → v.optional(v.array(v.float64())) */ build() { return this.convexValidator; } }; function vector(a, b) { if (typeof a === "string") { if (b === void 0) throw new Error("vector(name, dimensions) requires a dimensions number as the second argument"); return new ConvexVectorBuilder(a, b); } return new ConvexVectorBuilder("", a); } //#endregion //#region src/orm/constraints.ts var ConvexUniqueConstraintBuilderOn = class { static [entityKind] = "ConvexUniqueConstraintBuilderOn"; [entityKind] = "ConvexUniqueConstraintBuilderOn"; constructor(name) { this.name = name; } on(...columns) { return new ConvexUniqueConstraintBuilder(this.name, columns); } }; var ConvexUniqueConstraintBuilder = class { static [entityKind] = "ConvexUniqueConstraintBuilder"; [entityKind] = "ConvexUniqueConstraintBuilder"; config; constructor(name, columns) { this.config = { name, columns, nullsNotDistinct: false }; } nullsNotDistinct() { this.config.nullsNotDistinct = true; return this; } }; var ConvexForeignKeyBuilder = class { static [entityKind] = "ConvexForeignKeyBuilder"; [entityKind] = "ConvexForeignKeyBuilder"; config; constructor(config) { this.config = { ...config, onDelete: config.onDelete, onUpdate: config.onUpdate }; } onUpdate(action) { this.config.onUpdate = action; return this; } onDelete(action) { this.config.onDelete = action; return this; } }; function unique(name) { return new ConvexUniqueConstraintBuilderOn(name); } function foreignKey(config) { return new ConvexForeignKeyBuilder(config); } var ConvexCheckBuilder = class { static [entityKind] = "ConvexCheckBuilder"; [entityKind] = "ConvexCheckBuilder"; config; constructor(name, expression) { this.config = { name, expression }; } }; function check(name, expression) { return new ConvexCheckBuilder(name, expression); } //#endregion //#region src/orm/index-utils.ts function getIndexes(table) { const indexes = table.getIndexes?.(); return Array.isArray(indexes) ? indexes : []; } function getAggregateIndexes(table) { const indexes = table.getAggregateIndexes?.(); return Array.isArray(indexes) ? indexes : []; } function getRankIndexes(table) { const indexes = table.getRankIndexes?.(); return Array.isArray(indexes) ? indexes : []; } function getSearchIndexes(table) { const indexes = table.getSearchIndexes?.(); return Array.isArray(indexes) ? indexes : []; } function findSearchIndexByName(table, indexName) { return getSearchIndexes(table).find((index) => index.name === indexName) ?? null; } function getVectorIndexes(table) { const indexes = table.getVectorIndexes?.(); return Array.isArray(indexes) ? indexes : []; } function findVectorIndexByName(table, indexName) { return getVectorIndexes(table).find((index) => index.name === indexName) ?? null; } function findIndexForColumns(indexes, columns) { for (const index of indexes) { if (index.fields.length < columns.length) continue; let matches = true; for (let i = 0; i < columns.length; i++) if (index.fields[i] !== columns[i]) { matches = false; break; } if (matches) return index.name; } return null; } function findRelationIndex(table, columns, relationName, targetTableName, strict = true, allowFullScan = false) { const index = findIndexForColumns(getIndexes(table), columns); if (!index && !allowFullScan) throw new Error(`Relation ${relationName} requires index on '${targetTableName}(${columns.join(", ")})'. Set allowFullScan: true to override.`); if (!index && strict) console.warn(`Relation ${relationName} running without index (allowFullScan: true).`); return index; } //#endregion //#region src/orm/timestamp-mode.ts const PUBLIC_CREATED_AT_FIELD = "createdAt"; const INTERNAL_CREATION_TIME_FIELD = "_creationTime"; const CREATED_AT_MIGRATION_MESSAGE = "`_creationTime` is no longer public. Use `createdAt` instead."; const hasUserCreatedAtColumn = (table) => { if (!table || typeof table !== "object") return false; const columns = table[Columns]; if (!columns || typeof columns !== "object") return false; return Object.hasOwn(columns, PUBLIC_CREATED_AT_FIELD); }; const usesSystemCreatedAtAlias = (_table) => true; //#endregion //#region src/orm/mutation-utils.ts const UNDEFINED_SENTINEL_KEY = "__kitcnUndefined"; const INTERNAL_ID_FIELD$2 = "_id"; const PUBLIC_ID_FIELD$2 = "id"; const DATE_COLUMN_TYPE = "ConvexDate"; const TIMESTAMP_COLUMN_TYPE = "ConvexTimestamp"; const isPlainObject$1 = (value) => !!value && typeof value === "object" && !Array.isArray(value); const temporalColumnDescriptorCache = /* @__PURE__ */ new WeakMap(); const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/; const toDateOnlyString = (value) => value.toISOString().slice(0, 10); const toDateOnlyDate = (value) => DATE_ONLY_REGEX.test(value) ? /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`) : value; const toTimestampMillis = (value) => { if (value instanceof Date) return value.getTime(); if (typeof value === "string") { const parsed = Date.parse(value); if (!Number.isNaN(parsed)) return parsed; } return value; }; const readTimestampValue = (value, mode) => { if (mode === "date" && typeof value === "number") return new Date(value); if (mode === "string" && typeof value === "number") return new Date(value).toISOString(); return value; }; const getTemporalDescriptorFromColumn = (name, column) => { const config = column?.config; const columnType = config?.columnType; if (columnType !== DATE_COLUMN_TYPE && columnType !== TIMESTAMP_COLUMN_TYPE) return; if (columnType === DATE_COLUMN_TYPE) return { name, columnType, mode: config?.mode === "date" ? "date" : "string" }; return { name, columnType, mode: config?.mode === "string" ? "string" : "date" }; }; const getTemporalColumnDescriptors = (table) => { const cacheKey = table; const cached = temporalColumnDescriptorCache.get(cacheKey); if (cached) return cached; const descriptors = /* @__PURE__ */ new Map(); for (const [name, column] of Object.entries(getTableColumns$2(table))) { const descriptor = getTemporalDescriptorFromColumn(name, column); if (!descriptor) continue; descriptors.set(name, descriptor); } temporalColumnDescriptorCache.set(cacheKey, descriptors); return descriptors; }; const getTemporalColumnDescriptor = (table, columnName) => { const temporalColumns = getTemporalColumnDescriptors(table); const direct = temporalColumns.get(columnName); if (direct) return direct; const columns = getTableColumns$2(table); for (const descriptor of temporalColumns.values()) if (columns[descriptor.name]?.config?.name === columnName) return descriptor; }; const normalizeTemporalWriteValue = (descriptor, value) => { if (descriptor.columnType === DATE_COLUMN_TYPE) { if (value instanceof Date) return toDateOnlyString(value); return value; } return toTimestampMillis(value); }; const hydrateTemporalReadValue = (descriptor, value) => { if (descriptor.columnType === DATE_COLUMN_TYPE) { if (descriptor.mode === "date" && typeof value === "string") return toDateOnlyDate(value); return value; } return readTimestampValue(value, descriptor.mode); }; const normalizeTemporalComparableValue = (table, fieldName, value) => { const descriptor = getTemporalColumnDescriptor(table, fieldName); if (!descriptor) return value; if (Array.isArray(value)) return value.map((entry) => normalizeTemporalWriteValue(descriptor, entry)); return normalizeTemporalWriteValue(descriptor, value); }; const normalizePublicSystemFields = (value, options) => { if (!isPlainObject$1(value)) return value; const hasId = Object.hasOwn(value, INTERNAL_ID_FIELD$2); const hasCreationTime = Object.hasOwn(value, INTERNAL_CREATION_TIME_FIELD); if (!hasId && !hasCreationTime) return value; const obj = value; const { [INTERNAL_ID_FIELD$2]: internalId, ...rest } = obj; const publicRow = { ...rest }; if (hasId) publicRow[PUBLIC_ID_FIELD$2] = internalId; if (hasCreationTime) { const raw = obj[INTERNAL_CREATION_TIME_FIELD]; if (options?.useSystemCreatedAtAlias && raw !== void 0) publicRow[PUBLIC_CREATED_AT_FIELD] = raw; delete publicRow[INTERNAL_CREATION_TIME_FIELD]; } delete publicRow[INTERNAL_ID_FIELD$2]; return publicRow; }; const normalizeDateFieldsForWrite = (table, value) => { const useSystemCreatedAt = usesSystemCreatedAtAlias(table); const hasUserCreatedAt = hasUserCreatedAtColumn(table); const temporalColumns = getTemporalColumnDescriptors(table); const result = { ...value }; if (Object.hasOwn(result, INTERNAL_CREATION_TIME_FIELD)) throw new Error(CREATED_AT_MIGRATION_MESSAGE); if (useSystemCreatedAt && !hasUserCreatedAt && Object.hasOwn(result, PUBLIC_CREATED_AT_FIELD)) delete result[PUBLIC_CREATED_AT_FIELD]; for (const [name, descriptor] of temporalColumns.entries()) { if (!Object.hasOwn(result, name)) continue; result[name] = normalizeTemporalWriteValue(descriptor, result[name]); } return result; }; const hydrateDateFieldsForRead = (table, value) => { const rawCreationTime = isPlainObject$1(value) && typeof value[INTERNAL_CREATION_TIME_FIELD] === "number" ? value[INTERNAL_CREATION_TIME_FIELD] : void 0; const base = normalizePublicSystemFields(value, { useSystemCreatedAtAlias: usesSystemCreatedAtAlias(table) }); if (!isPlainObject$1(base)) return base; const result = { ...base }; const temporalColumns = getTemporalColumnDescriptors(table); for (const [name, descriptor] of temporalColumns.entries()) { if (name === PUBLIC_CREATED_AT_FIELD && result[name] === void 0 && rawCreationTime !== void 0) { result[name] = hydrateTemporalReadValue(descriptor, rawCreationTime); continue; } if (!Object.hasOwn(result, name)) continue; result[name] = hydrateTemporalReadValue(descriptor, result[name]); } return result; }; const selectReturningRowWithHydration = (table, row, fields) => { const useSystemCreatedAt = usesSystemCreatedAtAlias(table); const temporalColumns = getTemporalColumnDescriptors(table); const selected = {}; for (const [selectedKey, column] of Object.entries(fields)) { const columnName = getSelectionColumnName(column); let value = row[columnName]; if (!(columnName === INTERNAL_CREATION_TIME_FIELD && useSystemCreatedAt)) { const descriptor = temporalColumns.get(columnName); if (descriptor) value = hydrateTemporalReadValue(descriptor, value); } selected[selectedKey] = value; } return { ...selected }; }; const encodeUndefinedDeep = (value) => { if (value === void 0) return { [UNDEFINED_SENTINEL_KEY]: true }; if (Array.isArray(value)) return value.map((item) => encodeUndefinedDeep(item)); if (isPlainObject$1(value)) { const result = {}; for (const [key, nested] of Object.entries(value)) result[key] = encodeUndefinedDeep(nested); return result; } return value; }; const decodeUndefinedDeep = (value) => { if (Array.isArray(value)) return value.map((item) => decodeUndefinedDeep(item)); if (isPlainObject$1(value)) { if (Object.keys(value).length === 1 && value[UNDEFINED_SENTINEL_KEY] === true) return; const result = {}; for (const [key, nested] of Object.entries(value)) result[key] = decodeUndefinedDeep(nested); return result; } return value; }; const isSerializedFieldReference = (value) => isPlainObject$1(value) && typeof value.fieldName === "string"; const createBinaryExpression = (operator, fieldName, value) => { return { type: "binary", operator, operands: [fieldRef(fieldName), value], accept(visitor) { return visitor.visitBinary(this); } }; }; const createLogicalExpression = (operator, operands) => ({ type: "logical", operator, operands, accept(visitor) { return visitor.visitLogical(this); } }); const createUnaryExpression = (operator, operand) => ({ type: "unary", operator, operands: [operand], accept(visitor) { return visitor.visitUnary(this); } }); const serializeFilterExpression = (expression) => { if (!expression) return; if (expression.type === "binary") { const binary = expression; const [field, value] = binary.operands; if (!isFieldReference(field)) throw new Error("Binary expression must have FieldReference as first operand"); return { type: "binary", operator: binary.operator, field: { fieldName: field.fieldName }, value: encodeUndefinedDeep(value) }; } if (expression.type === "logical") { const logical = expression; return { type: "logical", operator: logical.operator, operands: logical.operands.map((operand) => serializeFilterExpression(operand)) }; } const unary = expression; const [operand] = unary.operands; return { type: "unary", operator: unary.operator, operand: isFieldReference(operand) ? { fieldName: operand.fieldName } : serializeFilterExpression(operand) }; }; const deserializeFilterExpression = (expression) => { if (!expression) return; if (expression.type === "binary") { const binary = expression; return createBinaryExpression(binary.operator, binary.field.fieldName, decodeUndefinedDeep(binary.value)); } if (expression.type === "logical") { const logical = expression; return createLogicalExpression(logical.operator, logical.operands.map((operand) => deserializeFilterExpression(operand)).filter((operand) => !!operand)); } const unary = expression; const operand = unary.operand; if (isSerializedFieldReference(operand)) return createUnaryExpression(unary.operator, fieldRef(operand.fieldName)); const nested = deserializeFilterExpression(operand); if (!nested) throw new Error("Serialized unary operand is missing."); return createUnaryExpression(unary.operator, nested); }; const DEFAULT_MUTATION_BATCH_SIZE = 400; const DEFAULT_MUTATION_LEAF_BATCH_SIZE = 1600; const DEFAULT_MUTATION_MAX_ROWS = 1e4; const DEFAULT_MUTATION_MAX_BYTES_PER_BATCH = 2097152; const DEFAULT_MUTATION_SCHEDULE_CALL_CAP = 800; const DEFAULT_MUTATION_ASYNC_DELAY_MS = 0; const DEFAULT_COUNT_BACKFILL_BATCH_SIZE = 1e3; const DEFAULT_RELATION_FAN_OUT_MAX_KEYS$1 = 1e3; const DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS$2 = 4096; const DEFAULT_AGGREGATE_WORK_BUDGET$2 = 16384; const MEASURED_BYTE_SAFETY_MULTIPLIER = 2; const UTF8_LENGTH_THRESHOLD = 500; const UTF8_ENCODER = new TextEncoder(); const getUtf8ByteLength = (value) => { if (value.length > UTF8_LENGTH_THRESHOLD) return UTF8_ENCODER.encode(value).length; let bytes = 0; for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); if (code < 128) bytes += 1; else if (code < 2048) bytes += 2; else if (code >= 55296 && code <= 56319) { bytes += 4; i += 1; } else bytes += 3; } return bytes; }; const resolveOrmRuntimeDefaults = (defaults, runtime = {}) => { const inferredMode = runtime.scheduler && runtime.scheduledMutationBatch ? "async" : "sync"; return { defaultLimit: defaults?.defaultLimit, countBackfillBatchSize: defaults?.countBackfillBatchSize ?? DEFAULT_COUNT_BACKFILL_BATCH_SIZE, relationFanOutMaxKeys: defaults?.relationFanOutMaxKeys ?? DEFAULT_RELATION_FAN_OUT_MAX_KEYS$1, aggregateCartesianMaxKeys: defaults?.aggregateCartesianMaxKeys ?? DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS$2, aggregateWorkBudget: defaults?.aggregateWorkBudget ?? DEFAULT_AGGREGATE_WORK_BUDGET$2, mutationBatchSize: defaults?.mutationBatchSize ?? DEFAULT_MUTATION_BATCH_SIZE, mutationLeafBatchSize: defaults?.mutationLeafBatchSize ?? DEFAULT_MUTATION_LEAF_BATCH_SIZE, mutationMaxRows: defaults?.mutationMaxRows ?? DEFAULT_MUTATION_MAX_ROWS, mutationMaxBytesPerBatch: defaults?.mutationMaxBytesPerBatch ?? DEFAULT_MUTATION_MAX_BYTES_PER_BATCH, mutationScheduleCallCap: defaults?.mutationScheduleCallCap ?? DEFAULT_MUTATION_SCHEDULE_CALL_CAP, mutationExecutionMode: defaults?.mutationExecutionMode ?? inferredMode, mutationAsyncDelayMs: defaults?.mutationAsyncDelayMs ?? DEFAULT_MUTATION_ASYNC_DELAY_MS }; }; const estimateMeasuredMutationRowBytes = (row) => { return getUtf8ByteLength(JSON.stringify(row)) * MEASURED_BYTE_SAFETY_MULTIPLIER; }; const takeRowsWithinByteBudget = (rows, maxBytesPerBatch) => { if (!Number.isInteger(maxBytesPerBatch) || maxBytesPerBatch < 1) throw new Error("mutationMaxBytesPerBatch must be a positive integer."); if (rows.length === 0) return { rows, hitLimit: false }; let bytes = 0; const selected = []; for (const row of rows) { const rowBytes = estimateMeasuredMutationRowBytes(row); if (selected.length > 0 && bytes + rowBytes > maxBytesPerBatch) return { rows: selected, hitLimit: true }; selected.push(row); bytes += rowBytes; } return { rows: selected, hitLimit: false }; }; const getMutationCollectionLimits = (context) => { const defaults = context?.resolvedDefaults ?? context?.defaults; const batchSize = defaults?.mutationBatchSize ?? DEFAULT_MUTATION_BATCH_SIZE; const leafBatchSize = defaults?.mutationLeafBatchSize ?? DEFAULT_MUTATION_LEAF_BATCH_SIZE; const maxRows = defaults?.mutationMaxRows ?? DEFAULT_MUTATION_MAX_ROWS; const maxBytesPerBatch = defaults?.mutationMaxBytesPerBatch ?? DEFAULT_MUTATION_MAX_BYTES_PER_BATCH; const scheduleCallCap = defaults?.mutationScheduleCallCap ?? DEFAULT_MUTATION_SCHEDULE_CALL_CAP; if (!Number.isInteger(batchSize) || batchSize < 1) throw new Error("mutationBatchSize must be a positive integer."); if (!Number.isInteger(leafBatchSize) || leafBatchSize < 1) throw new Error("mutationLeafBatchSize must be a positive integer."); if (!Number.isInteger(maxRows) || maxRows < 1) throw new Error("mutationMaxRows must be a positive integer."); if (!Number.isInteger(maxBytesPerBatch) || maxBytesPerBatch < 1) throw new Error("mutationMaxBytesPerBatch must be a positive integer."); if (!Number.isInteger(scheduleCallCap) || scheduleCallCap < 1) throw new Error("mutationScheduleCallCap must be a positive integer."); return { batchSize, leafBatchSize, maxRows, maxBytesPerBatch, scheduleCallCap }; }; const consumeScheduleCall = (state) => { if (!state) return; if (state.remainingCalls < 1) throw new Error(`Async cascade scheduling exceeded mutationScheduleCallCap (${state.callCap}). Increase defineSchema(..., { defaults: { mutationScheduleCallCap } }) or reduce fan-out per mutation.`); state.remainingCalls -= 1; }; const getMutationExecutionMode = (context, override) => { const requestedMode = override ?? context?.resolvedDefaults?.mutationExecutionMode ?? context?.defaults?.mutationExecutionMode; if (requestedMode === "sync") return "sync"; if (requestedMode === "async") { if (override === "async") return "async"; if (context?.scheduler && context?.scheduledMutationBatch) return "async"; return "sync"; } if (context?.scheduler && context?.scheduledMutationBatch) return "async"; return "sync"; }; const getMutationAsyncDelayMs = (context, override) => override ?? context?.resolvedDefaults?.mutationAsyncDelayMs ?? context?.defaults?.mutationAsyncDelayMs ?? DEFAULT_MUTATION_ASYNC_DELAY_MS; const collectMutationRowsBounded = async (buildQuery, options) => { const rows = await buildQuery().take(options.maxRows + 1); if (rows.length > options.maxRows) throw new Error(`${options.operation} matched more than ${options.maxRows} rows on "${options.tableName}". Narrow the filter or increase defineSchema(..., { defaults: { mutationMaxRows } }).`); return rows; }; function getTableName(table) { const name = table.tableName ?? table[TableName] ?? table?._?.name; if (!name) throw new Error("Table is missing a name"); return name; } function getTableDeleteConfig(table) { return table[TableDeleteConfig]; } function getUniqueIndexes(table) { const fromMethod = table.getUniqueIndexes?.(); if (Array.isArray(fromMethod)) return fromMethod; const fromField = table.uniqueIndexes; return Array.isArray(fromField) ? fromField : []; } function getChecks(table) { const fromMethod = table.getChecks?.(); if (Array.isArray(fromMethod)) return fromMethod; const fromField = table.checks; return Array.isArray(fromField) ? fromField : []; } function buildForeignKeyGraph(schema) { const tableByName = /* @__PURE__ */ new Map(); for (const tableConfig of Object.values(schema)) if (tableConfig?.name && tableConfig.table) tableByName.set(tableConfig.name, tableConfig.table); const incomingByTable = /* @__PURE__ */ new Map(); for (const tableConfig of Object.values(schema)) { const sourceTable = tableConfig.table; const sourceTableName = tableConfig.name; const foreignKeys = getForeignKeys(sourceTable); for (const foreignKey of foreignKeys) { const targetTableName = foreignKey.foreignTableName; if (!tableByName.get(targetTableName)) throw new Error(`Foreign key from '${sourceTableName}' references missing table '${targetTableName}'.`); const entry = { sourceTable, sourceTableName, sourceColumns: foreignKey.columns, targetTableName, targetColumns: foreignKey.foreignColumns, onDelete: foreignKey.onDelete, onUpdate: foreignKey.onUpdate }; const list = incomingByTable.get(targetTableName) ?? []; list.push(entry); incomingByTable.set(targetTableName, list); } } return { incomingByTable }; } function getOrmContext(db) { return db[OrmContext]; } function getForeignKeys(table) { const fromMethod = table.getForeignKeys?.(); if (Array.isArray(fromMethod)) return fromMethod; const fromField = table.foreignKeys; return Array.isArray(fromField) ? fromField : []; } function getColumnName$1(column) { const name = column.config?.name ?? column?._?.name; if (!name) throw new Error("Column builder is missing a column name"); return name; } function getTableColumns$2(table) { return table[Columns] ?? {}; } function getTablePolymorphicConfigs(table) { const fromMethod = table.getPolymorphicConfigs?.(); if (Array.isArray(fromMethod)) return fromMethod; const fromSymbol = table[TablePolymorphic]; return Array.isArray(fromSymbol) ? fromSymbol : []; } function enforcePolymorphicWrite(table, candidate, options) { const configs = getTablePolymorphicConfigs(table); if (configs.length === 0) return; for (const config of configs) { const changedFields = options?.changedFields; const discriminatorChanged = !changedFields || changedFields.has(config.discriminator); const generatedFieldChanged = !changedFields || config.generatedFieldNames.some((fieldName) => changedFields.has(fieldName)); if (!discriminatorChanged && !generatedFieldChanged) continue; const discriminatorValue = candidate[config.discriminator]; if (typeof discriminatorValue !== "string" || !Object.hasOwn(config.variants, discriminatorValue)) throw new Error(`Invalid discriminator '${config.discriminator}' on '${getTableName(table)}'. Expected one of: ${Object.keys(config.variants).join(", ")}.`); const activeVariant = config.variants[discriminatorValue]; if (!activeVariant) throw new Error(`Invalid discriminator '${config.discriminator}' value '${discriminatorValue}' on '${getTableName(table)}'.`); for (const requiredFieldName of activeVariant.requiredFieldNames) { const value = candidate[requiredFieldName]; if (value === null || value === void 0) throw new Error(`discriminator branch '${discriminatorValue}' requires '${requiredFieldName}' on '${getTableName(table)}'.`); } const activeFieldSet = new Set(activeVariant.fieldNames); for (const generatedFieldName of config.generatedFieldNames) { if (activeFieldSet.has(generatedFieldName)) continue; const value = candidate[generatedFieldName]; if (value !== null && value !== void 0) throw new Error(`discriminator branch '${discriminatorValue}' cannot set '${generatedFieldName}' on '${getTableName(table)}' because it belongs to another variant.`); } } } function getColumnConfig(table, columnName) { const builder = getTableColumns$2(table)[columnName]; if (!builder) return null; return builder.config ?? null; } function applyDefaults(table, value) { const columns = table[Columns]; if (!columns) return value; const result = { ...value }; const polymorphicConfigs = getTablePolymorphicConfigs(table); const activeGeneratedFields = /* @__PURE__ */ new Set(); const generatedFields = /* @__PURE__ */ new Set(); for (const config of polymorphicConfigs) { for (const fieldName of config.generatedFieldNames) generatedFields.add(fieldName); const discriminatorValue = result[config.discriminator]; if (typeof discriminatorValue !== "string") continue; const activeVariant = config.variants[discriminatorValue]; if (!activeVariant) continue; for (const fieldName of activeVariant.fieldNames) activeGeneratedFields.add(fieldName); } for (const [columnName, builder] of Object.entries(columns)) { if (result[columnName] !== void 0) continue; if (generatedFields.has(columnName) && !activeGeneratedFields.has(columnName)) continue; const config = builder.config; if (!config) continue; if (typeof config.defaultFn === "function") { result[columnName] = config.defaultFn(); continue; } if (config.hasDefault) { result[columnName] = config.default; continue; } if (typeof config.onUpdateFn === "function") result[columnName] = config.onUpdateFn(); } return result; } async function enforceUniqueIndexes(db, table, candidate, options) { const uniqueIndexes = getUniqueIndexes(table); if (uniqueIndexes.length === 0) return; const tableName = getTableName(table); const changedFields = options?.changedFields; for (const index of uniqueIndexes) { if (changedFields && !index.fields.some((field) => changedFields.has(field))) continue; const entries = index.fields.map((field) => [field, candidate[field]]); if (entries.some(([, value]) => value === void 0 || value === null) && !index.nullsNotDistinct) continue; const existing = await db.query(tableName).withIndex(index.name, (q) => { let builder = q.eq(entries[0][0], entries[0][1]); for (let i = 1; i < entries.length; i++) builder = builder.eq(entries[i][0], entries[i][1]); return builder; }).unique(); if (existing !== null && (options?.currentId === void 0 || existing._id !== options.currentId)) throw new Error(`Unique index '${index.name}' violation on '${tableName}'.`); } } async function enforceForeignKeys(db, table, candidate, options) { const foreignKeys = getForeignKeys(table); if (foreignKeys.length === 0) return; const tableName = getTableName(table); const changedFields = options?.changedFields; for (const foreignKey of foreignKeys) { if (changedFields && !foreignKey.columns.some((field) => changedFields.has(field))) continue; const entries = foreignKey.columns.map((field) => [field, candidate[field]]); if (entries.some(([, value]) => value === void 0 || value === null)) continue; if (foreignKey.foreignColumns.length === 1 && foreignKey.foreignColumns[0] === "_id") { const foreignId = entries[0]?.[1]; if (!await db.get(foreignId)) throw new Error(`Foreign key violation on '${tableName}': missing document in '${foreignKey.foreignTableName}'.`); continue; } if (!foreignKey.foreignTable) throw new Error(`Foreign key on '${tableName}' requires indexed foreign columns on '${foreignKey.foreignTableName}'.`); const indexName = findIndexForColumns(getIndexes(foreignKey.foreignTable), foreignKey.foreignColumns); if (!indexName) throw new Error(`Foreign key on '${tableName}' requires index on '${foreignKey.foreignTableName}(${foreignKey.foreignColumns.join(", ")})'.`); if (!await db.query(foreignKey.foreignTableName).withIndex(indexName, (q) => { let builder = q.eq(foreignKey.foreignColumns[0], entries[0][1]); for (let i = 1; i < entries.length; i++) builder = builder.eq(foreignKey.foreignColumns[i], entries[i][1]); return builder; }).first()) throw new Error(`Foreign key violation on '${tableName}': missing document in '${foreignKey.foreignTableName}'.`); } } function getIndexForForeignKey(foreignKey) { return findIndexForColumns(getIndexes(foreignKey.sourceTable), foreignKey.sourceColumns); } function foreignKeyIndexError(foreignKey) { return /* @__PURE__ */ new Error(`Foreign key on '${foreignKey.sourceTableName}' requires index on '${foreignKey.sourceTableName}(${foreignKey.sourceColumns.join(", ")})' for cascading actions.`); } function buildIndexPredicate(q, columns, values) { let builder = q.eq(columns[0], values[0]); for (let i = 1; i < columns.length; i++) builder = builder.eq(columns[i], values[i]); return builder; } function buildFilterPredicate(q, columns, values) { let expr = q.eq(q.field(columns[0]), values[0]); for (let i = 1; i < columns.length; i++) expr = q.and(expr, q.eq(q.field(columns[i]), values[i])); return expr; } function ensureNullableColumns(table, columns, context) { for (const columnName of columns) { const config = getColumnConfig(table, columnName); if (!config) throw new Error(`${context}: missing column '${columnName}' in table '${getTableName(table)}'.`); if (config.notNull) throw new Error(`${context}: column '${columnName}' is not nullable in '${getTableName(table)}'.`); } } function ensureDefaultColumns(table, columns, context) { const defaults = {}; for (const columnName of columns) { const config = getColumnConfig(table, columnName); if (!config) throw new Error(`${context}: missing column '${columnName}' in table '${getTableName(table)}'.`); if (!config.hasDefault) throw new Error(`${context}: column '${columnName}' has no default in '${getTableName(table)}'.`); defaults[columnName] = config.default; } return defaults; } function ensureNonNullValues(table, values, context) { for (const [columnName, value] of Object.entries(values)) if (getColumnConfig(table, columnName)?.notNull && (value === null || value === void 0)) throw new Error(`${context}: column '${columnName}' cannot be null in '${getTableName(table)}'.`); } async function collectReferencingRows(db, foreignKey, targetValues, indexName, options) { return collectMutationRowsBounded(() => db.query(foreignKey.sourceTableName).withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, targetValues)), { operation: options.operation, tableName: foreignKey.sourceTableName, batchSize: options.batchSize, maxRows: options.maxRows }); } async function collectAsyncCascadeRowsBounded(buildQuery, batchSize, maxBytesPerBatch) { const rows = await buildQuery().take(batchSize + 1); const hasMoreRows = rows.length > batchSize; const bounded = takeRowsWithinByteBudget(hasMoreRows ? rows.slice(0, batchSize) : rows, maxBytesPerBatch); return { rows: bounded.rows, needsContinuation: hasMoreRows || bounded.hitLimit }; } async function hasReferencingRow(db, foreignKey, targetValues, indexName) { const query = db.query(foreignKey.sourceTableName); return (indexName ? await query.withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, targetValues)).first() : await query.filter((q) => buildFilterPredicate(q, foreignKey.sourceColumns, targetValues)).first()) !== null; } async function softDeleteRow(db, table, row) { const tableName = getTableName(table); if (!("deletionTime" in getTableColumns$2(table))) throw new Error(`Soft delete requires 'deletionTime' field on '${tableName}'.`); const deletionTime = Date.now(); await db.patch(tableName, row._id, { deletionTime }); return deletionTime; } async function hardDeleteRow(db, _tableName, row) { await db.delete(row._id); } async function applyIncomingForeignKeyActionsOnDelete(db, table, row, options) { const tableName = getTableName(table); const incoming = options.graph.incomingByTable.get(tableName) ?? []; if (incoming.length === 0) return; for (const foreignKey of incoming) { const action = foreignKey.onDelete ?? "no action"; const targetValues = foreignKey.targetColumns.map((column) => row[column]); if (targetValues.some((value) => value === void 0 || value === null)) continue; const indexName = getIndexForForeignKey(foreignKey); if (action === "restrict" || action === "no action") { if (!indexName && !options.allowFullScan) throw foreignKeyIndexError(foreignKey); if (!indexName && options.strict) console.warn(`Foreign key check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`); if (await hasReferencingRow(db, foreignKey, targetValues, indexName)) throw new Error(`Foreign key restrict violation on '${tableName}' from '${foreignKey.sourceTableName}'.`); continue; } if (!indexName) { if (!options.allowFullScan) throw foreignKeyIndexError(foreignKey); if (options.strict) console.warn(`Foreign key cascade check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`); if (await hasReferencingRow(db, foreignKey, targetValues, null)) throw foreignKeyIndexError(foreignKey); continue; } let referencingRows; if (options.executionMode === "async") { const asyncBatchSize = action === "cascade" ? options.batchSize : options.leafBatchSize; const { rows, needsContinuation } = await collectAsyncCascadeRowsBounded(() => db.query(foreignKey.sourceTableName).withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, targetValues)), asyncBatchSize, options.maxBytesPerBatch); referencingRows = rows; if (needsContinuation) { if (!options.scheduler || !options.scheduledMutationBatch) throw new Error("Async mutation execution requires orm.db(ctx) configured with scheduling (ormFunctions.scheduledMutationBatch)."); consumeScheduleCall(options.scheduleState); await options.scheduler.runAfter(options.delayMs ?? 0, options.scheduledMutationBatch, { workType: "cascade-delete", mode: "async", operation: "delete", table: foreignKey.sourceTableName, foreignIndexName: indexName, foreignSourceColumns: foreignKey.sourceColumns, targetValues: encodeUndefinedDeep(targetValues), foreignAction: action, deleteMode: options.deleteMode, cascadeMode: options.cascadeMode, cursor: null, batchSize: asyncBatchSize, maxBytesPerBatch: options.maxBytesPerBatch, delayMs: options.delayMs ?? 0 }); } } else referencingRows = await collectReferencingRows(db, foreignKey, targetValues, indexName, { operation: "delete", batchSize: options.batchSize, maxRows: options.maxRows }); if (referencingRows.length === 0) continue; if (action === "set null") { ensureNullableColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set null on '${foreignKey.sourceTableName}'`); for (const referencingRow of referencingRows) { const patch = {}; for (const columnName of foreignKey.sourceColumns) patch[columnName] = null; await db.patch(foreignKey.sourceTableName, referencingRow._id, patch); } continue; } if (action === "set default") { const defaults = ensureDefaultColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set default on '${foreignKey.sourceTableName}'`); for (const referencingRow of referencingRows) await db.patch(foreignKey.sourceTableName, referencingRow._id, defaults); continue; } if (action === "cascade") for (const referencingRow of referencingRows) { const key = `${foreignKey.sourceTableName}:${referencingRow._id}`; if (options.visited.has(key)) continue; options.visited.add(key); await applyIncomingForeignKeyActionsOnDelete(db, foreignKey.sourceTable, referencingRow, options); if (options.cascadeMode === "soft") await softDeleteRow(db, foreignKey.sourceTable, referencingRow); else await hardDeleteRow(db, foreignKey.sourceTableName, referencingRow); } } } async function applyIncomingForeignKeyActionsOnUpdate(db, table, oldRow, newRow, options) { const tableName = getTableName(table); const incoming = options.graph.incomingByTable.get(tableName) ?? []; if (incoming.length === 0) return; for (const foreignKey of incoming) { const action = foreignKey.onUpdate ?? "no action"; const oldValues = foreignKey.targetColumns.map((column) => oldRow[column]); const newValues = foreignKey.targetColumns.map((column) => newRow[column]); if (!oldValues.some((value, index) => !Object.is(value, newValues[index]))) continue; if (oldValues.some((value) => value === void 0 || value === null)) continue; const indexName = getIndexForForeignKey(foreignKey); if (action === "restrict" || action === "no action") { if (!indexName && !options.allowFullScan) throw foreignKeyIndexError(foreignKey); if (!indexName && options.strict) console.warn(`Foreign key check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`); if (await hasReferencingRow(db, foreignKey, oldValues, indexName)) throw new Error(`Foreign key restrict violation on '${tableName}' from '${foreignKey.sourceTableName}'.`); continue; } if (!indexName) { if (!options.allowFullScan) throw foreignKeyIndexError(foreignKey); if (options.strict) console.warn(`Foreign key cascade check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`); if (await hasReferencingRow(db, foreignKey, oldValues, null)) throw foreignKeyIndexError(foreignKey); continue; } let referencingRows; if (options.executionMode === "async") { const asyncBatchSize = options.leafBatchSize; const { rows, needsContinuation } = await collectAsyncCascadeRowsBounded(() => db.query(foreignKey.sourceTableName).withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, oldValues)), asyncBatchSize, options.maxBytesPerBatch); referencingRows = rows; if (needsContinuation) { if (!options.scheduler || !options.scheduledMutationBatch) throw new Error("Async mutation execution requires orm.db(ctx) configured with scheduling (ormFunctions.scheduledMutationBatch)."); consumeScheduleCall(options.scheduleState); await options.scheduler.runAfter(options.delayMs ?? 0, options.scheduledMutationBatch, { workType: "cascade-update", mode: "async", operation: "update", table: foreignKey.sourceTableName, foreignIndexName: indexName, foreignSourceColumns: foreignKey.sourceColumns, targetValues: encodeUndefinedDeep(oldValues), newValues: encodeUndefinedDeep(newValues), foreignAction: action, cursor: null, batchSize: asyncBatchSize, maxBytesPerBatch: options.maxBytesPerBatch, delayMs: options.delayMs ?? 0 }); } } else referencingRows = await collectReferencingRows(db, foreignKey, oldValues, indexName, { operation: "update", batchSize: options.batchSize, maxRows: options.maxRows }); if (referencingRows.length === 0) continue; if (action === "set null") { ensureNullableColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set null on '${foreignKey.sourceTableName}'`); for (const referencingRow of referencingRows) { const patch = {}; for (const columnName of foreignKey.sourceColumns) patch[columnName] = null; await db.patch(foreignKey.sourceTableName, referencingRow._id, patch); } continue; } if (action === "set default") { const defaults = ensureDefaultColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set default on '${foreignKey.sourceTableName}'`); for (const referencingRow of referencingRows) await db.patch(foreignKey.sourceTableName, referencingRow._id, defaults); continue; } if (action === "cascade") { const patchValues = {}; for (let i = 0; i < foreignKey.sourceColumns.length; i++) patchValues[foreignKey.sourceColumns[i]] = newValues[i]; ensureNonNullValues(foreignKey.sourceTable, patchValues, `Foreign key cascade update on '${foreignKey.sourceTableName}'`); for (const referencingRow of referencingRows) await db.patch(foreignKey.sourceTableName, referencingRow._id, patchValues); } } } function getSelectionColumnName(value) { if (value && typeof value === "object") { if ("columnName" in value) return value.columnName; if ("config" in value && value.config?.name) return value.config.name; } throw new Error("Returning selection must reference a column"); } function splitReturningSelection(fields) { const columnSelection = {}; let countSelection; for (const [key, value] of Object.entries(fields)) { if (key !== "_count") { getSelectionColumnName(value); columnSelection[key] = value; continue; } if (value === void 0) continue; if (!isPlainObject$1(value)) throw new Error("returning({ _count }) requires an object."); const nextCountSelection = {}; for (const [relationName, relationSelection] of Object.entries(value)) { if (relationSelection === void 0 || relationSelection === false) continue; if (relationSelection === true) { nextCountSelection[relationName] = true; continue; } if (!isPlainObject$1(relationSelection)) throw new Error(`returning({ _count.${relationName} }) must be true or { where }`); if ("select" in relationSelection) throw new Error(`returning({ _count.${relationName}.select }) is removed. Use returning({ _count: { ${relationName}: true } })`); for (const [optionKey, optionValue] of Object.entries(relationSelection)) if (optionKey !== "where" && optionValue !== void 0) throw new Error(`returning({ _count.${relationName} }) does not support '${optionKey}'`); if (typeof relationSelection.where === "function") throw new Error(`returning({ _count.${relationName}.where }) callback is unsupported in v1`); nextCountSelection[relationName] = { where: relationSelection.where }; } countSelection = nextCountSelection; } return { columnSelection: Object.keys(columnSelection).length > 0 ? columnSelection : void 0, countSelection }; } function matchLike(value, pattern, caseInsensitive) { const targetValue = caseInsensitive ? value.toLowerCase() : value; const targetPattern = caseInsensitive ? pattern.toLowerCase() : pattern; if (targetPattern.startsWith("%") && targetPattern.endsWith("%")) { const substring = targetPattern.slice(1, -1); return targetValue.includes(substring); } if (targetPattern.startsWith("%")) { const suffix = targetPattern.slice(1); return targetValue.endsWith(suffix); } if (targetPattern.endsWith("%")) { const prefix = targetPattern.slice(0, -1); return targetValue.startsWith(prefix); } return targetValue === targetPattern; } function evaluat