jexidb
Version:
JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.
298 lines (256 loc) • 8.59 kB
JavaScript
/**
* SchemaManager - Manages field schemas for optimized array-based serialization
* This replaces the need for repeating field names in JSON objects
*/
export default class SchemaManager {
constructor(opts = {}) {
this.opts = Object.assign({
enableArraySerialization: true,
strictSchema: true,
debugMode: false
}, opts)
// Schema definition: array of field names in order
this.schema = []
this.fieldToIndex = new Map() // field name -> index
this.indexToField = new Map() // index -> field name
this.schemaVersion = 1
this.isInitialized = false
}
/**
* Initialize schema from options or auto-detect from data
*/
initializeSchema(schemaOrData, autoDetect = false) {
if (this.isInitialized && this.opts.strictSchema) {
if (this.opts.debugMode) {
console.log('SchemaManager: Schema already initialized, skipping')
}
return
}
if (Array.isArray(schemaOrData)) {
// Explicit schema provided
this.setSchema(schemaOrData)
} else if (autoDetect && typeof schemaOrData === 'object') {
// Auto-detect schema from data
this.autoDetectSchema(schemaOrData)
} else if (schemaOrData && typeof schemaOrData === 'object') {
// Initialize from database options
this.initializeFromOptions(schemaOrData)
}
this.isInitialized = true
if (this.opts.debugMode) {
console.log('SchemaManager: Schema initialized:', this.schema)
}
}
/**
* Set explicit schema
*/
setSchema(fieldNames) {
this.schema = [...fieldNames] // Create copy
this.fieldToIndex.clear()
this.indexToField.clear()
this.schema.forEach((field, index) => {
this.fieldToIndex.set(field, index)
this.indexToField.set(index, field)
})
if (this.opts.debugMode) {
console.log('SchemaManager: Schema set:', this.schema)
}
}
/**
* Auto-detect schema from sample data
*/
autoDetectSchema(sampleData) {
if (Array.isArray(sampleData)) {
// Use first record as template
if (sampleData.length > 0) {
this.autoDetectSchema(sampleData[0])
}
return
}
if (typeof sampleData === 'object' && sampleData !== null) {
const fields = Object.keys(sampleData).sort() // Sort for consistency
// CRITICAL FIX: Always include 'id' field in schema for proper array format
if (!fields.includes('id')) {
fields.push('id')
}
this.setSchema(fields)
}
}
/**
* Initialize schema from database options
* Note: schema option is no longer supported, use fields instead
*/
initializeFromOptions(opts) {
// Schema option is no longer supported - fields should be used instead
// This method is kept for compatibility but does nothing
// Schema initialization is handled by Database.initializeSchema() using fields
}
/**
* Add new field to schema (for schema evolution)
*/
addField(fieldName) {
if (this.fieldToIndex.has(fieldName)) {
return this.fieldToIndex.get(fieldName)
}
const newIndex = this.schema.length
this.schema.push(fieldName)
this.fieldToIndex.set(fieldName, newIndex)
this.indexToField.set(newIndex, fieldName)
if (this.opts.debugMode) {
console.log('SchemaManager: Added field:', fieldName, 'at index:', newIndex)
}
return newIndex
}
/**
* Convert object to array using schema with strict field enforcement
*/
objectToArray(obj) {
if (!this.isInitialized || !this.opts.enableArraySerialization) {
return obj // Fallback to object format
}
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return obj // Don't convert non-objects or arrays
}
const result = new Array(this.schema.length)
// Fill array with values in schema order
// Missing fields become undefined, extra fields are ignored
for (let i = 0; i < this.schema.length; i++) {
const fieldName = this.schema[i]
result[i] = obj[fieldName] !== undefined ? obj[fieldName] : undefined
}
// CRITICAL FIX: Always append 'id' field if it exists and is not in schema
// The 'id' field must be preserved even if not in the schema
if (obj.id !== undefined && obj.id !== null && this.schema.indexOf('id') === -1) {
result.push(obj.id)
}
return result
}
/**
* Convert array back to object using schema
*/
arrayToObject(arr) {
if (!this.isInitialized || !this.opts.enableArraySerialization) {
return arr // Fallback to array format
}
if (!Array.isArray(arr)) {
return arr // Don't convert non-arrays
}
const obj = {}
const idIndex = this.schema.indexOf('id')
// DISABLED: Schema migration detection was causing field mapping corruption
// The logic was incorrectly assuming ID was in first position when it's appended at the end
// This caused fields to be shifted incorrectly during object-to-array-to-object conversion
let arrayOffset = 0
// Map array values to object properties
// Only include fields that are in the schema
for (let i = 0; i < Math.min(arr.length - arrayOffset, this.schema.length); i++) {
const fieldName = this.schema[i]
const arrayIndex = i + arrayOffset
// Only include non-undefined values to avoid cluttering the object
if (arr[arrayIndex] !== undefined) {
obj[fieldName] = arr[arrayIndex]
}
}
// CRITICAL FIX: Always preserve 'id' field if it exists in the original object
// The 'id' field may not be in the schema but must be preserved
if (idIndex !== -1 && arr[idIndex] !== undefined) {
// 'id' is in schema and has a value
obj.id = arr[idIndex]
} else if (!obj.id && arr.length > this.schema.length + arrayOffset) {
// 'id' is not in schema but array has extra element(s) - check if last element could be ID
// This handles cases where ID was added after schema initialization
for (let i = this.schema.length + arrayOffset; i < arr.length; i++) {
// Try to infer if this is an ID (string that looks like an ID)
const potentialId = arr[i]
if (potentialId !== undefined && potentialId !== null && typeof potentialId === 'string' && potentialId.length > 0 && potentialId.length < 100) {
obj.id = potentialId
break // Use first potential ID found
}
}
}
return obj
}
/**
* Get field index by name
*/
getFieldIndex(fieldName) {
return this.fieldToIndex.get(fieldName)
}
/**
* Get field name by index
*/
getFieldName(index) {
return this.indexToField.get(index)
}
/**
* Check if field exists in schema
*/
hasField(fieldName) {
return this.fieldToIndex.has(fieldName)
}
/**
* Get schema as array of field names
*/
getSchema() {
return [...this.schema] // Return copy
}
/**
* Get schema size
*/
getSchemaSize() {
return this.schema.length
}
/**
* Validate that object conforms to schema
*/
validateObject(obj) {
if (!this.isInitialized || !this.opts.strictSchema) {
return true
}
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false
}
// Check if object has all required fields
for (const field of this.schema) {
if (!(field in obj)) {
if (this.opts.debugMode) {
console.warn('SchemaManager: Missing required field:', field)
}
return false
}
}
return true
}
/**
* Get schema metadata for serialization
*/
getSchemaMetadata() {
return {
version: this.schemaVersion,
fields: [...this.schema],
fieldCount: this.schema.length,
isInitialized: this.isInitialized
}
}
/**
* Reset schema
*/
reset() {
this.schema = []
this.fieldToIndex.clear()
this.indexToField.clear()
this.isInitialized = false
this.schemaVersion++
}
/**
* Get performance statistics
*/
getStats() {
return {
schemaSize: this.schema.length,
isInitialized: this.isInitialized,
version: this.schemaVersion,
enableArraySerialization: this.opts.enableArraySerialization
}
}
}