@iarayan/ch-orm
Version:
A Developer-First ClickHouse ORM with Powerful CLI Tools
415 lines • 16.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManyToMany = exports.BelongsTo = exports.HasMany = exports.HasOne = exports.Relationship = void 0;
exports.Relationships = Relationships;
const ModelDecorators_1 = require("../decorators/ModelDecorators");
/**
* Base class for all relationship types
* Provides common functionality for relationship management
*/
class Relationship {
/**
* Constructor for relationship
* @param ownerModel - The model instance that owns this relationship
* @param relatedModelClass - The related model class
* @param localKey - The local key (default: primary key of owner model)
*/
constructor(ownerModel, relatedModelClass, localKey) {
this.ownerModel = ownerModel;
this.relatedModelClass = relatedModelClass;
this.localKey = localKey || this.getOwnerPrimaryKey();
}
/**
* Get owner model primary key
*/
getOwnerPrimaryKey() {
const primaryKeys = ModelDecorators_1.MetadataStorage.getPrimaryKeys(this.ownerModel.constructor);
if (primaryKeys.length === 0) {
throw new Error(`No primary key defined for model ${this.ownerModel.constructor.name}`);
}
return primaryKeys[0];
}
/**
* Get related model primary key
*/
getRelatedPrimaryKey() {
const primaryKeys = ModelDecorators_1.MetadataStorage.getPrimaryKeys(this.relatedModelClass);
if (primaryKeys.length === 0) {
throw new Error(`No primary key defined for model ${this.relatedModelClass.name}`);
}
return primaryKeys[0];
}
/**
* Get the related model table name
*/
getRelatedTableName() {
const tableName = ModelDecorators_1.MetadataStorage.getTableName(this.relatedModelClass);
if (!tableName) {
throw new Error(`No table name defined for model ${this.relatedModelClass.name}`);
}
return tableName;
}
/**
* Get the owner model table name
*/
getOwnerTableName() {
const tableName = ModelDecorators_1.MetadataStorage.getTableName(this.ownerModel.constructor);
if (!tableName) {
throw new Error(`No table name defined for model ${this.ownerModel.constructor.name}`);
}
return tableName;
}
/**
* Get the connection from a model instance
*/
getConnection() {
// Using the static getConnection method from Model
return this.relatedModelClass.getConnection();
}
/**
* Get the query builder for the related model
*/
getQueryBuilder() {
// Use the static method on the Model class instead of an instance method
return this.relatedModelClass.query();
}
}
exports.Relationship = Relationship;
/**
* HasOne relationship
* Represents a one-to-one relationship from the owner model to the related model
*/
class HasOne extends Relationship {
/**
* Constructor for HasOne relationship
* @param ownerModel - The model instance that owns this relationship
* @param relatedModelClass - The related model class
* @param foreignKey - The foreign key on the related model
* @param localKey - The local key (default: primary key of owner model)
*/
constructor(ownerModel, relatedModelClass, foreignKey, localKey) {
super(ownerModel, relatedModelClass, localKey);
this.foreignKey = foreignKey;
}
/**
* Get the related record
*/
async get() {
const localKeyValue = this.ownerModel[this.localKey];
return this.getQueryBuilder()
.where(this.foreignKey, "=", localKeyValue)
.limit(1)
.get();
}
/**
* Get the first related record (convenience method)
*/
async first() {
const results = await this.get();
return results.length > 0 ? results[0] : null;
}
/**
* Eager load related records for a collection of models
*/
async eagerLoad(models) {
// Extract the local key values from all models
const localKeyValues = models.map((model) => model[this.localKey]);
// Query the related records
const relatedRecords = await this.getQueryBuilder()
.whereIn(this.foreignKey, localKeyValues)
.get();
// Group related records by the foreign key value
const recordMap = new Map();
for (const record of relatedRecords) {
const foreignKeyValue = record[this.foreignKey];
if (!recordMap.has(foreignKeyValue)) {
recordMap.set(foreignKeyValue, []);
}
recordMap.get(foreignKeyValue).push(record);
}
return recordMap;
}
}
exports.HasOne = HasOne;
/**
* HasMany relationship
* Represents a one-to-many relationship from the owner model to the related model
*/
class HasMany extends Relationship {
/**
* Constructor for HasMany relationship
* @param ownerModel - The model instance that owns this relationship
* @param relatedModelClass - The related model class
* @param foreignKey - The foreign key on the related model
* @param localKey - The local key (default: primary key of owner model)
*/
constructor(ownerModel, relatedModelClass, foreignKey, localKey) {
super(ownerModel, relatedModelClass, localKey);
this.foreignKey = foreignKey;
}
/**
* Get the related records
*/
async get() {
const localKeyValue = this.ownerModel[this.localKey];
return this.getQueryBuilder()
.where(this.foreignKey, "=", localKeyValue)
.get();
}
/**
* Eager load related records for a collection of models
*/
async eagerLoad(models) {
// Extract the local key values from all models
const localKeyValues = models.map((model) => model[this.localKey]);
// Query the related records
const relatedRecords = await this.getQueryBuilder()
.whereIn(this.foreignKey, localKeyValues)
.get();
// Group related records by the foreign key value
const recordMap = new Map();
for (const record of relatedRecords) {
const foreignKeyValue = record[this.foreignKey];
if (!recordMap.has(foreignKeyValue)) {
recordMap.set(foreignKeyValue, []);
}
recordMap.get(foreignKeyValue).push(record);
}
return recordMap;
}
}
exports.HasMany = HasMany;
/**
* BelongsTo relationship
* Represents an inverse one-to-one or one-to-many relationship
*/
class BelongsTo extends Relationship {
/**
* Constructor for BelongsTo relationship
* @param ownerModel - The model instance that owns this relationship
* @param relatedModelClass - The related model class
* @param foreignKey - The foreign key on the owner model
* @param ownerKey - The referenced key on the related model (default: primary key)
*/
constructor(ownerModel, relatedModelClass, foreignKey, ownerKey) {
super(ownerModel, relatedModelClass, ownerKey || null);
this.foreignKey = foreignKey;
// If no owner key is provided, use the primary key of the related model
if (!ownerKey) {
this.localKey = this.getRelatedPrimaryKey();
}
}
/**
* Get the related record
*/
async get() {
const foreignKeyValue = this.ownerModel[this.foreignKey];
return this.getQueryBuilder()
.where(this.localKey, "=", foreignKeyValue)
.limit(1)
.get();
}
/**
* Get the first related record (convenience method)
*/
async first() {
const results = await this.get();
return results.length > 0 ? results[0] : null;
}
/**
* Eager load related records for a collection of models
*/
async eagerLoad(models) {
// Extract the foreign key values from all models
const foreignKeyValues = models.map((model) => model[this.foreignKey]);
// Query the related records
const relatedRecords = await this.getQueryBuilder()
.whereIn(this.localKey, foreignKeyValues)
.get();
// Group related records by the owner key value
const recordMap = new Map();
for (const record of relatedRecords) {
const ownerKeyValue = record[this.localKey];
if (!recordMap.has(ownerKeyValue)) {
recordMap.set(ownerKeyValue, []);
}
recordMap.get(ownerKeyValue).push(record);
}
return recordMap;
}
}
exports.BelongsTo = BelongsTo;
/**
* ManyToMany relationship
* Represents a many-to-many relationship through a pivot table
*/
class ManyToMany extends Relationship {
/**
* Constructor for ManyToMany relationship
* @param ownerModel - The model instance that owns this relationship
* @param relatedModelClass - The related model class
* @param pivotTable - The pivot table name
* @param foreignPivotKey - The foreign key on the pivot table for the owner model
* @param relatedPivotKey - The foreign key on the pivot table for the related model
* @param localKey - The local key on the owner model (default: primary key)
* @param relatedKey - The local key on the related model (default: primary key)
*/
constructor(ownerModel, relatedModelClass, pivotTable, foreignPivotKey, relatedPivotKey, localKey, relatedKey) {
super(ownerModel, relatedModelClass, localKey);
this.relatedKey = relatedKey;
this.pivotTable = pivotTable;
this.foreignPivotKey = foreignPivotKey;
this.relatedPivotKey = relatedPivotKey;
this.relatedKey = relatedKey || this.getRelatedPrimaryKey();
}
/**
* Get the related records
*/
async get() {
const localKeyValue = this.ownerModel[this.localKey];
const relatedTable = this.getRelatedTableName();
const queryBuilder = this.getQueryBuilder();
// Create a query builder for the related model and use raw SQL for join
return queryBuilder.rawQuery(`
SELECT ${relatedTable}.*
FROM ${relatedTable}
INNER JOIN ${this.pivotTable} ON ${relatedTable}.${this.relatedKey} = ${this.pivotTable}.${this.relatedPivotKey}
WHERE ${this.pivotTable}.${this.foreignPivotKey} = '${localKeyValue}'
`);
}
/**
* Eager load related records for a collection of models
*/
async eagerLoad(models) {
// Extract the local key values from all models
const localKeyValues = models.map((model) => model[this.localKey]);
const relatedTable = this.getRelatedTableName();
const localKeyValuesString = localKeyValues.map((v) => `'${v}'`).join(",");
// Query using raw SQL for the join
const queryBuilder = this.getQueryBuilder();
const relatedRecords = await queryBuilder.rawQuery(`
SELECT ${relatedTable}.*, ${this.pivotTable}.${this.foreignPivotKey} as pivot_foreign_key
FROM ${relatedTable}
INNER JOIN ${this.pivotTable} ON ${relatedTable}.${this.relatedKey} = ${this.pivotTable}.${this.relatedPivotKey}
WHERE ${this.pivotTable}.${this.foreignPivotKey} IN (${localKeyValuesString})
`);
// Group related records by the pivot foreign key value
const recordMap = new Map();
for (const record of relatedRecords) {
// Get the pivot foreign key from the record
const foreignKeyValue = record.pivot_foreign_key;
if (!recordMap.has(foreignKeyValue)) {
recordMap.set(foreignKeyValue, []);
}
recordMap.get(foreignKeyValue).push(record);
}
return recordMap;
}
/**
* Attach related models to the owner model
* @param relatedIds - IDs of related models to attach
*/
async attach(relatedIds) {
const ids = Array.isArray(relatedIds) ? relatedIds : [relatedIds];
const localKeyValue = this.ownerModel[this.localKey];
// Prepare the pivot data for insertion
const pivotData = ids.map((id) => ({
[this.foreignPivotKey]: localKeyValue,
[this.relatedPivotKey]: id,
}));
// Get the connection
const connection = this.getConnection();
// Insert into the pivot table
await connection.insert(this.pivotTable, pivotData);
}
/**
* Detach related models from the owner model
* @param relatedIds - Optional IDs of related models to detach. Detaches all if not provided.
*/
async detach(relatedIds) {
const localKeyValue = this.ownerModel[this.localKey];
// Get the connection
const connection = this.getConnection();
// Build the query to delete from the pivot table
let query = `DELETE FROM ${this.pivotTable} WHERE ${this.foreignPivotKey} = '${localKeyValue}'`;
// If specific IDs are provided, add them to the query
if (relatedIds !== undefined) {
const ids = Array.isArray(relatedIds) ? relatedIds : [relatedIds];
const idsString = ids.map((id) => `'${id}'`).join(", ");
query += ` AND ${this.relatedPivotKey} IN (${idsString})`;
}
// Execute the delete query
await connection.query(query);
}
/**
* Toggle the attachment status of the given related models
* @param relatedIds - IDs of related models to toggle
*/
async toggle(relatedIds) {
const ids = Array.isArray(relatedIds) ? relatedIds : [relatedIds];
const localKeyValue = this.ownerModel[this.localKey];
// Get the connection
const connection = this.getConnection();
// Get existing attached IDs
const query = `SELECT ${this.relatedPivotKey} FROM ${this.pivotTable} WHERE ${this.foreignPivotKey} = '${localKeyValue}'`;
const result = await connection.query(query);
const existingIds = result.data.map((row) => row[this.relatedPivotKey]);
// Determine which IDs to attach and which to detach
const idsToAttach = ids.filter((id) => !existingIds.includes(id));
const idsToDetach = ids.filter((id) => existingIds.includes(id));
// Attach and detach as needed
if (idsToAttach.length > 0) {
await this.attach(idsToAttach);
}
if (idsToDetach.length > 0) {
await this.detach(idsToDetach);
}
}
}
exports.ManyToMany = ManyToMany;
/**
* Class decorator to register relationships for a model
* @param relationships - Map of relationship property names to relationship definitions
*/
function Relationships(relationships) {
return function (target) {
// Store the original constructor
const originalConstructor = target;
// Create a new constructor
const newConstructor = function (...args) {
// Call the original constructor
const instance = new originalConstructor(...args);
// Define getters for each relationship
for (const [property, relationshipFactory] of Object.entries(relationships)) {
Object.defineProperty(instance, property, {
get: function () {
// Create the relationship instance
const relationship = relationshipFactory(instance);
// Return an object that can be called to execute the relationship query
const relationshipProxy = new Proxy(relationship, {
// When the property is called as a function, execute the relationship query
apply: function (target, thisArg, args) {
return relationship.get();
},
// Support property access on the relationship instance
get: function (target, prop) {
return target[prop];
},
});
return relationshipProxy;
},
enumerable: true,
configurable: true,
});
}
return instance;
};
// Copy prototype so intanceof works correctly
newConstructor.prototype = originalConstructor.prototype;
// Copy static properties
Object.setPrototypeOf(newConstructor, originalConstructor);
// Return the new constructor
return newConstructor;
};
}
//# sourceMappingURL=ModelRelationships.js.map