adonis-odm
Version:
A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support
228 lines (227 loc) • 6.89 kB
JavaScript
import { EmbeddedQueryBuilder } from './embedded_query_builder.js';
import { EmbeddedModelInstance } from './embedded_model_instance.js';
/**
* Proxy for single embedded documents with full CRUD capabilities
* Returns an EmbeddedModelInstance that behaves exactly like a regular model
*/
export class EmbeddedSingleProxy {
_value = null;
_parent;
_fieldName;
_modelClass;
constructor(parent, fieldName, modelClass, initialValue) {
this._parent = parent;
this._fieldName = fieldName;
this._modelClass = modelClass;
if (initialValue) {
this.setValue(initialValue);
}
}
/**
* Set the embedded value
*/
setValue(value) {
if (value === null || value === undefined) {
this._value = null;
}
else {
// Create an EmbeddedModelInstance that behaves like a full model
this._value = new EmbeddedModelInstance(this._modelClass, this._parent, this._fieldName, value, false // not an array item
);
}
this.markParentDirty();
}
/**
* Get the embedded value
*/
getValue() {
return this._value;
}
/**
* Create a new embedded instance
*/
create(attributes) {
const instance = new EmbeddedModelInstance(this._modelClass, this._parent, this._fieldName, attributes, false // not an array item
);
this._value = instance;
this.markParentDirty();
return instance;
}
/**
* Clear the embedded value
*/
clear() {
this._value = null;
this.markParentDirty();
}
/**
* Mark parent model as dirty
*/
markParentDirty() {
this._parent.setAttribute(this._fieldName, this._value);
}
/**
* Serialize for database storage
*/
toDocument() {
if (!this._value)
return null;
return this._value.toDocument ? this._value.toDocument() : this._value;
}
}
/**
* Proxy for multiple embedded documents with full CRUD capabilities
* Returns an array where each item is an EmbeddedModelInstance
*/
export class EmbeddedManyProxy extends Array {
_parent;
_fieldName;
_modelClass;
constructor(parent, fieldName, modelClass, initialValue) {
super();
this._parent = parent;
this._fieldName = fieldName;
this._modelClass = modelClass;
// Set up the prototype chain properly - this is crucial for maintaining methods
Object.setPrototypeOf(this, EmbeddedManyProxy.prototype);
// Ensure the constructor property is correct
Object.defineProperty(this, 'constructor', {
value: EmbeddedManyProxy,
writable: true,
configurable: true,
});
if (initialValue && Array.isArray(initialValue)) {
this.setItems(initialValue);
}
}
/**
* Set the items array
*/
setItems(items) {
// Clear existing items
this.length = 0;
// Add new items as EmbeddedModelInstances
items.forEach((item, index) => {
const embeddedInstance = new EmbeddedModelInstance(this._modelClass, this._parent, this._fieldName, item, true, // is array item
index);
// Use Array.prototype.push to avoid triggering our overridden push
Array.prototype.push.call(this, embeddedInstance);
});
// Ensure prototype chain is maintained after modifications
Object.setPrototypeOf(this, EmbeddedManyProxy.prototype);
this.markParentDirty();
}
/**
* Query the embedded documents
*/
query() {
return new EmbeddedQueryBuilder(Array.from(this));
}
/**
* Create a new embedded document
*/
create(attributes) {
const instance = new EmbeddedModelInstance(this._modelClass, this._parent, this._fieldName, attributes, true, // is array item
this.length);
this.push(instance);
this.markParentDirty();
return instance;
}
/**
* Create multiple embedded documents
*/
createMany(attributesArray) {
const instances = attributesArray.map((attributes, index) => {
return new EmbeddedModelInstance(this._modelClass, this._parent, this._fieldName, attributes, true, // is array item
this.length + index);
});
this.push(...instances);
this.markParentDirty();
return instances;
}
/**
* Remove an embedded document
*/
remove(item) {
const index = this.indexOf(item);
if (index > -1) {
this.splice(index, 1);
this.updateArrayIndices();
this.markParentDirty();
return true;
}
return false;
}
/**
* Remove embedded documents by condition
*/
removeWhere(callback) {
const initialLength = this.length;
// Filter out items that match the condition
for (let i = this.length - 1; i >= 0; i--) {
if (callback(this[i])) {
this.splice(i, 1);
}
}
const removedCount = initialLength - this.length;
if (removedCount > 0) {
this.updateArrayIndices();
this.markParentDirty();
}
return removedCount;
}
/**
* Override push to maintain array indices
*/
push(...items) {
const result = super.push(...items);
this.updateArrayIndices();
this.markParentDirty();
return result;
}
/**
* Override pop to maintain array indices
*/
pop() {
const result = super.pop();
this.updateArrayIndices();
this.markParentDirty();
return result;
}
/**
* Override splice to maintain array indices
*/
splice(start, deleteCount, ...items) {
const result = super.splice(start, deleteCount ?? 0, ...items);
this.updateArrayIndices();
this.markParentDirty();
return result;
}
/**
* Update array indices for all items
*/
updateArrayIndices() {
for (let i = 0; i < this.length; i++) {
if (this[i] && typeof this[i] === 'object' && '_arrayIndex' in this[i]) {
;
this[i]._arrayIndex = i;
}
}
}
/**
* Mark parent model as dirty
*/
markParentDirty() {
// Don't call setAttribute as it might interfere with the proxy
// Instead, directly mark the field as dirty if the parent is persisted
if (this._parent.$isPersisted) {
this._parent.$dirty[this._fieldName] = this;
}
}
/**
* Serialize for database storage
*/
toDocument() {
return this.map((item) => (item && item.toDocument ? item.toDocument() : item));
}
}