import _ from "lodash" import { flattenContents } from "." import { SchemaJson, Table, Column, Variable, Expr } from "./types" import produce from "immer" type ColumnMap = { [columnId: string]: Column } /** Abstracted schema for a database. Immutable. * Stores tables with columns (possibly in nested sections). * See types.ts for more info. * * Also contains any variables and their values. * * Variables can be either bound or unbound. If they are bound, it means that they currently have a value within the schema. * If they are unbound, then the expression cannot be compiled, but can still be validated. */ export default class Schema { tables: Table[] /** Map of table.id to table */ tableMap: { [tableId: string]: Table } /** Map of "" to map of { "" to column } */ columnMaps: { [tableId: string]: ColumnMap } /** Variables in the schema */ variables: Variable[] /** Values of variables */ variableValues: { [variableId: string]: Expr } constructor(schemaJson?: SchemaJson, variables?: Variable[], variableValues?: { [variableId: string]: Expr }) { this.tables = [] this.tableMap = {} this.columnMaps = {} this.variables = variables || [] this.variableValues = variableValues || {} if (schemaJson) { this.tables = schemaJson.tables // Setup maps for (let table of this.tables) { this.tableMap[table.id] = table this.columnMaps[table.id] = this.indexTable(table) } } } /** Add a single variable, replacing any existing one with the same id */ addVariable(variable: Variable, value?: Expr) { if (value === undefined) { return this.addVariables([variable], {}) } return this.addVariables([variable], { [variable.id]: value }) } /** Add variables, replacing any existing ones with the same id */ addVariables(variables: Variable[], variableValues: { [variableId: string]: Expr }) { const schema = new Schema() schema.tables = this.tables schema.tableMap = this.tableMap schema.columnMaps = this.columnMaps schema.variables = produce(this.variables, draft => { for (let variable of variables) { const index = draft.findIndex(v => v.id === variable.id) if (index !== -1) { draft[index] = variable } else { draft.push(variable) } } }) schema.variableValues = produce(this.variableValues, draft => { for (let variableId in variableValues) { draft[variableId] = variableValues[variableId] } }) return schema } private indexTable(table: Table): ColumnMap { return _.indexBy(flattenContents(table.contents), (c) => c.id) } getTables() { return this.tables } getTable(tableId: string): Table | null { return this.tableMap[tableId] || null } getColumn(tableId: string, columnId: string): Column | null { const map = this.columnMaps[tableId] if (!map) { return null } return map[columnId] || null } /** Gets the columns in order, flattened out from sections */ getColumns(tableId: string) { return flattenContents(this.getTable(tableId)!.contents) } getVariables() { return this.variables } getVariableValues() { return this.variableValues } /** Add table with id, name, desc, primaryKey, ordering (column with natural order) and contents (array of columns/sections) * Will replace table if already exists. * schemas are immutable, so returns a fresh copy */ addTable(table: Table): Schema { // Remove existing and add new const newTables = this.tables.filter((t) => t.id !== table.id) newTables.push(table) // Update table map const newTableMap = { ...this.tableMap, [table.id]: table } // Update column map const newIndex = this.indexTable(table) const newColumnMaps = { ...this.columnMaps, [table.id]: newIndex } // Create new schema const schema = new Schema() schema.tables = newTables schema.tableMap = newTableMap schema.columnMaps = newColumnMaps return schema } // Convert to a JSON toJSON(): SchemaJson { return { tables: this.tables } } }