UNPKG

convex-ents

Version:

Relations, default values, unique fields, RLS for Convex

466 lines (465 loc) 18.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/schema.ts var schema_exports = {}; __export(schema_exports, { defineEnt: () => defineEnt, defineEntFromTable: () => defineEntFromTable, defineEntSchema: () => defineEntSchema, defineEntsFromTables: () => defineEntsFromTables, edgeCompoundIndexName: () => edgeCompoundIndexName, getEntDefinitions: () => getEntDefinitions }); module.exports = __toCommonJS(schema_exports); var import_server = require("convex/server"); var import_values = require("convex/values"); function defineEntSchema(schema, options) { const tableNames = Object.keys(schema); for (const tableName of tableNames) { const table = schema[tableName]; for (const edge of edgeConfigsBeforeDefineSchema(table)) { if ( // Skip inverse edges, we process their forward edges edge.cardinality === "multiple" && edge.type === "ref" && (edge.inverse !== void 0 || // symmetric is only set by defineEntSchema, // so we already processed the pair edge.symmetric !== void 0) ) { continue; } const otherTableName = edge.to; if (otherTableName.startsWith("_")) { if (edge.cardinality !== "single") { throw new Error( `Many:many edge "${edge.name}" in table "${tableName}" points to a system table "${otherTableName}", but only 1:1 edges can point to system tables` ); } if (edge.type !== "field") { throw new Error( `Edge "${edge.name}" in table "${tableName}" pointing to a system table "${otherTableName}" must store the edge by storing the system document ID. Remove the \`ref\` option.` ); } if (edge.deletion === "soft") { throw new Error( `Edge "${edge.name}" in table "${tableName}" pointing to a system table "${otherTableName}" cannot use soft deletion, because system documents cannot be soft deleted.` ); } continue; } const otherTable = schema[otherTableName]; if (otherTable === void 0) { throw new Error( `Edge "${edge.name}" in table "${tableName}" points to an undefined table "${otherTableName}"` ); } const isSelfDirected = edge.to === tableName; const inverseEdgeCandidates = edgeConfigsBeforeDefineSchema( otherTable ).filter(canBeInverseEdge(tableName, edge, isSelfDirected)); if (inverseEdgeCandidates.length > 1) { throw new Error( `Edge "${edge.name}" in table "${tableName}" has too many potential inverse edges in table "${otherTableName}": ${inverseEdgeCandidates.map((edge2) => `"${edge2.name}"`).join(", ")}` ); } const inverseEdge = inverseEdgeCandidates[0]; if (edge.cardinality === "single" && edge.type === "field" && inverseEdge === void 0) { throw new Error( `Missing inverse edge in table "${otherTableName}" for edge "${edge.name}" in table "${tableName}"` ); } if (edge.cardinality === "single" && edge.type === "ref") { if (inverseEdge === void 0) { throw new Error( `Missing inverse edge in table "${otherTableName}" ${edge.ref !== null ? `with field "${edge.ref}" ` : ""}for edge "${edge.name}" in table "${tableName}"` ); } if (inverseEdge.cardinality === "single" && inverseEdge.type === "ref") { throw new Error( `Both edge "${edge.name}" in table "${inverseEdge.to}" and edge "${inverseEdge.name}" in table "${edge.to}" are marked as references, choose one to store the edge by removing the \`ref\` option.` ); } if (inverseEdge.cardinality !== "single" || inverseEdge.type !== "field") { throw new Error( `Unexpected inverse edge type ${edge.name}, ${inverseEdge?.name}` ); } if (edge.ref === null) { edge.ref = inverseEdge.field; } inverseEdge.unique = true; } if (edge.cardinality === "single" || edge.cardinality === "multiple" && edge.type === "field") { if (edge.deletion !== void 0 && deletionConfigFromEntDefinition(otherTable) === void 0) { throw new Error( `Cannot specify soft deletion behavior for edge "${edge.name}" in table "${tableName}" because the target table "${otherTableName}" does not have a "soft" or "scheduled" deletion behavior configured.` ); } } if (edge.cardinality === "multiple") { if (!isSelfDirected && inverseEdge === void 0) { throw new Error( `Missing inverse edge in table "${otherTableName}" for edge "${edge.name}" in table "${tableName}"` ); } if (inverseEdge?.cardinality === "single") { if (inverseEdge.type === "ref") { throw new Error( `The edge "${inverseEdge.name}" in table "${otherTableName}" specified \`ref\`, but it must store the 1:many edge as a field. Check the its inverse edge "${edge.name}" in table "${tableName}".` ); } if (edge.type === "ref") { throw new Error( `The edge "${inverseEdge.name}" in table "${otherTableName}" cannot be singular, as the edge "${edge.name}" in table "${tableName}" did not specify the \`ref\` option.` ); } edge.type = "field"; edge.ref = inverseEdge.field; } if (inverseEdge?.cardinality === "multiple" || isSelfDirected) { if (!isSelfDirected && edge?.type === "field") { throw new Error( `The edge "${edge.name}" in table "${tableName}" specified \`ref\`, but its inverse edge "${inverseEdge.name}" in table "${otherTableName}" is not the singular end of a 1:many edge.` ); } if (inverseEdge?.type === "field") { throw new Error( `The edge "${inverseEdge.name}" in table "${otherTableName}" specified \`ref\`, but its inverse edge "${edge.name}" in table "${tableName}" is not the singular end of a 1:many edge.` ); } const edgeTableName = edge.type === "ref" && edge.table !== void 0 ? edge.table : inverseEdge === void 0 ? `${tableName}_${edge.name}` : inverseEdge.name !== tableName ? `${tableName}_${inverseEdge.name}_to_${edge.name}` : `${inverseEdge.name}_to_${edge.name}`; const forwardId = edge.type === "ref" && edge.field !== void 0 ? edge.field : inverseEdge === void 0 ? "aId" : tableName === otherTableName ? inverseEdge.name + "Id" : tableName + "Id"; const inverseId = isSelfDirected && edge.type === "ref" && edge.inverseField !== void 0 ? edge.inverseField : inverseEdge === void 0 ? "bId" : inverseEdge.type === "ref" && inverseEdge.field !== void 0 ? inverseEdge.field : tableName === otherTableName ? edge.name + "Id" : otherTableName + "Id"; const edgeTable = defineEnt({ [forwardId]: import_values.v.id(tableName), [inverseId]: import_values.v.id(otherTableName) }).index(forwardId, [forwardId]).index(inverseId, [inverseId]).index(edgeCompoundIndexNameRaw(forwardId, inverseId), [ forwardId, inverseId ]); const isSymmetric = inverseEdge === void 0; if (!isSymmetric) { edgeTable.index(edgeCompoundIndexNameRaw(inverseId, forwardId), [ inverseId, forwardId ]); } schema[edgeTableName] = edgeTable; const edgeConfig = edge; edgeConfig.type = "ref"; edgeConfig.table = edgeTableName; edgeConfig.field = forwardId; edgeConfig.ref = inverseId; edgeConfig.symmetric = inverseEdge === void 0; if (inverseEdge !== void 0) { inverseEdge.type = "ref"; const inverseEdgeConfig = inverseEdge; inverseEdgeConfig.table = edgeTableName; inverseEdgeConfig.field = inverseId; inverseEdgeConfig.ref = forwardId; inverseEdgeConfig.symmetric = false; } } } } } return (0, import_server.defineSchema)(schema, options); } function edgeCompoundIndexName(edgeDefinition) { return edgeCompoundIndexNameRaw(edgeDefinition.field, edgeDefinition.ref); } function edgeCompoundIndexNameRaw(idA, idB) { return `${idA}_${idB}`; } function canBeInverseEdge(tableName, edge, isSelfDirected) { return (candidate) => { if (candidate.to !== tableName) { return false; } if (isSelfDirected) { return candidate.cardinality === "multiple" && candidate.type === "ref" && candidate.inverse === edge.name; } if (edge.cardinality === "single" && edge.type === "ref" && edge.ref !== null || edge.cardinality === "multiple" && edge.type === "field" && edge.ref !== true) { if (candidate.cardinality === "single" && candidate.type === "field") { return edge.ref === candidate.field; } } if (edge.cardinality === "single" && edge.type === "field" && edge.field !== null) { if (candidate.cardinality === "single" && candidate.type === "ref" && candidate.ref !== null || candidate.cardinality === "multiple" && candidate.type === "field" && candidate.ref !== true) { return edge.field === candidate.ref; } } if (edge.cardinality === "multiple" && edge.type === "ref" && edge.table !== void 0) { return candidate.cardinality === "multiple" && candidate.type === "ref" && edge.table === candidate.table; } if (candidate.cardinality === "multiple" && candidate.type === "ref" && candidate.table !== void 0) { return edge.cardinality === "multiple" && edge.type === "ref" && edge.table === candidate.table; } return true; }; } function edgeConfigsBeforeDefineSchema(table) { return Object.values( table.edgeConfigs ); } function deletionConfigFromEntDefinition(table) { return table.deletionConfig; } function defineEnt(documentSchema) { if (isValidator(documentSchema)) { if (!(documentSchema.kind === "object" || documentSchema.kind === "union" && documentSchema.members.every((member) => member.kind === "object"))) { throw new Error("Ent shape must be an object or a union of objects"); } } return new EntDefinitionImpl((0, import_values.asObjectValidator)(documentSchema)); } function isValidator(v2) { return !!v2.isConvexValidator; } function defineEntFromTable(definition) { const validator = definition.validator; if (validator.kind !== "object") { throw new Error( "Only tables with object definition are supported in Ents, not unions" ); } const entDefinition = defineEnt(validator.fields); entDefinition.indexes = definition.indexes; entDefinition.searchIndexes = definition.searchIndexes; entDefinition.vectorIndexes = definition.vectorIndexes; return entDefinition; } function defineEntsFromTables(definitions) { const result = {}; for (const key in definitions) { result[key] = defineEntFromTable(definitions[key]); } return result; } var EntDefinitionImpl = class { validator; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore indexes = []; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore searchIndexes = []; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore vectorIndexes = []; edgeConfigs = {}; fieldConfigs = {}; defaults = {}; deletionConfig; constructor(documentSchema) { this.validator = documentSchema; } index(name, fields) { this.indexes.push({ indexDescriptor: name, fields }); return this; } searchIndex(name, indexConfig) { this.searchIndexes.push({ indexDescriptor: name, searchField: indexConfig.searchField, filterFields: indexConfig.filterFields || [] }); return this; } vectorIndex(name, indexConfig) { this.vectorIndexes.push({ indexDescriptor: name, vectorField: indexConfig.vectorField, dimensions: indexConfig.dimensions, filterFields: indexConfig.filterFields || [] }); return this; } /** * Export the contents of this definition. * * This is called internally by the Convex framework. * @internal */ export() { return { indexes: this.indexes, searchIndexes: this.searchIndexes, vectorIndexes: this.vectorIndexes, documentType: this.validator.json }; } field(name, validator, options) { if (this._has(name)) { throw new Error(`Duplicate field "${name}"`); } const finalValidator = options?.default !== void 0 ? import_values.v.optional(validator) : validator; this._expand(name, finalValidator); if (options?.unique === true || options?.index === true) { this.indexes.push({ indexDescriptor: name, fields: [name] }); } if (options?.default !== void 0) { this.defaults[name] = options.default; } if (options?.unique === true) { this.fieldConfigs[name] = { name, unique: true }; } return this; } edge(edgeName, options) { if (this.edgeConfigs[edgeName] !== void 0) { throw new Error(`Duplicate edge "${edgeName}"`); } const to = options?.to ?? edgeName + "s"; if (options?.field !== void 0 && options?.ref !== void 0) { throw new Error( `Cannot specify both \`field\` and \`ref\` for the same edge, choose one to be the reference and the other to store the foreign key.` ); } if (options?.field !== void 0 || options?.ref === void 0) { const fieldName = options?.field ?? edgeName + "Id"; this._expand( fieldName, options?.optional === true ? import_values.v.optional(import_values.v.id(to)) : import_values.v.id(to) ); this.edgeConfigs[edgeName] = { name: edgeName, to, cardinality: "single", type: "field", field: fieldName, optional: options?.optional === true, deletion: options?.deletion }; this.indexes.push({ indexDescriptor: fieldName, fields: [fieldName] }); return this; } this.edgeConfigs[edgeName] = { name: edgeName, to, cardinality: "single", type: "ref", ref: options.ref === true ? null : options.ref, deletion: options.deletion }; return this; } edges(name, options) { const cardinality = "multiple"; const to = options?.to ?? name; const ref = options?.ref; const table = options?.table; if (ref !== void 0 && table !== void 0) { throw new Error( `Cannot specify both \`ref\` and \`table\` for the same edge, as the former is for 1:many edges and the latter for many:many edges. Config: \`${JSON.stringify(options)}\`` ); } const field = options?.field; const inverseField = options?.inverseField; if ((field !== void 0 || inverseField !== void 0) && table === void 0) { throw new Error( `Specify \`table\` if you're customizing the \`field\` or \`inverseField\` for a many:many edge. Config: \`${JSON.stringify(options)}\`` ); } const inverseName = options?.inverse; const deletion = options?.deletion; this.edgeConfigs[name] = ref !== void 0 ? { name, to, cardinality, type: "field", ref, deletion } : { name, to, cardinality, type: "ref", table, field, inverseField }; if (inverseName !== void 0) { this.edgeConfigs[inverseName] = { name: inverseName, to, cardinality, type: "ref", inverse: name, table }; } return this; } deletion(type, options) { if (this._has("deletionTime")) { throw new Error( `Cannot enable "${type}" deletion because "deletionTime" field was already defined.` ); } if (this.deletionConfig !== void 0) { throw new Error(`Deletion behavior can only be specified once.`); } this._expand("deletionTime", import_values.v.optional(import_values.v.number())); this.deletionConfig = { type, ...options }; return this; } _has(name) { if (this.validator.kind === "object") { return this.validator.fields[name] !== void 0; } if (this.validator.kind === "union") { return this.validator.members.some( (member) => member.kind === "object" && member.fields[name] !== void 0 ); } return false; } _expand(name, validator) { if (this.validator.kind === "object") { this.validator.fields[name] = validator; } if (this.validator.kind === "union") { this.validator.members.forEach((member) => { if (member.kind === "object") { member.fields[name] = validator; } }); } } }; function getEntDefinitions(schema) { const tables = schema.tables; return Object.entries(tables).reduce( (acc, [tableName, table]) => { acc[tableName] = { indexes: table.indexes.reduce( (acc2, { indexDescriptor, fields }) => { acc2[indexDescriptor] = fields; return acc2; }, {} ), defaults: table.defaults, edges: table.edgeConfigs, fields: table.fieldConfigs, deletionConfig: table.deletionConfig }; return acc; }, {} ); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { defineEnt, defineEntFromTable, defineEntSchema, defineEntsFromTables, edgeCompoundIndexName, getEntDefinitions }); //# sourceMappingURL=schema.js.map