@adonisjs/lucid
Version:
SQL ORM built on top of Active Record pattern
1,411 lines • 60.9 kB
JavaScript
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { DateTime } from 'luxon';
import Hooks from '@poppinss/hooks';
import lodash from '@poppinss/utils/lodash';
import { Exception, defineStaticProperty } from '@poppinss/utils';
import * as errors from '../../errors.js';
import { Preloader } from '../preloader/index.js';
import { proxyHandler } from './proxy_handler.js';
import { ModelKeys } from '../model_keys/index.js';
import { HasOne } from '../relations/has_one/index.js';
import { HasMany } from '../relations/has_many/index.js';
import { BelongsTo } from '../relations/belongs_to/index.js';
import { ManyToMany } from '../relations/many_to_many/index.js';
import { HasManyThrough } from '../relations/has_many_through/index.js';
import { CamelCaseNamingStrategy } from '../naming_strategies/camel_case.js';
import { LazyLoadAggregates } from '../relations/aggregates_loader/lazy_load.js';
import { isObject, collectValues, ensureRelation, managedTransaction, normalizeCherryPickObject, transformDateValue, compareValues, } from '../../utils/index.js';
const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough'];
const DATE_TIME_TYPES = {
date: 'date',
datetime: 'datetime',
};
function StaticImplements() {
return (_t) => { };
}
/**
* Abstract class to define fully fledged data models
*/
let BaseModelImpl = class BaseModelImpl {
/**
* The adapter to be used for persisting and fetching data.
*
* NOTE: Adapter is a singleton and share among all the models, unless
* a user wants to swap the adapter for a given model
*/
static $adapter;
/**
* Define an adapter to use for interacting with
* the database
*/
static useAdapter(adapter) {
this.$adapter = adapter;
}
/**
* Naming strategy for model properties
*/
static namingStrategy = new CamelCaseNamingStrategy();
/**
* Primary key is required to build relationships across models
*/
static primaryKey;
/**
* Whether the model has been booted. Booting the model initializes its
* static properties. Base models must not be initialized.
*/
static booted;
/**
* Query scopes defined on the model
*/
static $queryScopes = {};
/**
* A set of properties marked as computed. Computed properties are included in
* the `toJSON` result, else they behave the same way as any other instance
* property.
*/
static $computedDefinitions;
/**
* Columns makes it easier to define extra props on the model
* and distinguish them with the attributes to be sent
* over to the adapter
*/
static $columnsDefinitions;
/**
* Registered relationships for the given model
*/
static $relationsDefinitions;
/**
* The name of database table. It is auto generated from the model name, unless
* specified
*/
static table;
/**
* Self assign the primary instead of relying on the database to
* return it back
*/
static selfAssignPrimaryKey;
/**
* A custom connection to use for queries. The connection defined on
* query builder is preferred over the model connection
*/
static connection;
/**
* Storing model hooks
*/
static $hooks;
/**
* Keys mappings to make the lookups easy
*/
static $keys;
/**
* Creates a new model instance with payload and adapter options
*/
static newUpWithOptions(payload, options, allowExtraProperties) {
const row = new this();
row.merge(payload, allowExtraProperties);
/**
* Pass client options to the newly created row. If row was found
* the query builder will set the same options.
*/
row.$setOptionsAndTrx(options);
return row;
}
/**
* Helper method for `fetchOrNewUpMany`, `fetchOrCreateMany` and `createOrUpdate`
* many.
*/
static newUpIfMissing(rowObjects, existingRows, keys, mergeAttribute, options, allowExtraProperties) {
/**
* Return existing or create missing rows in the same order as the original
* array
*/
return rowObjects.map((rowObject) => {
const existingRow = existingRows.find((row) => {
return keys.every((key) => {
const objectValue = rowObject[key];
const rowValue = row[key];
return compareValues(rowValue, objectValue);
});
});
/**
* Return the row found from the select call
*/
if (existingRow) {
if (mergeAttribute) {
existingRow.merge(rowObject, allowExtraProperties);
}
return existingRow;
}
/**
* Otherwise create a new one
*/
return this.newUpWithOptions(rowObject, options, allowExtraProperties);
});
}
/**
* Returns the model query instance for the given model
*/
static query(options) {
return this.$adapter.query(this, options);
}
static async transaction(callbackOrOptions, options) {
if (typeof callbackOrOptions === 'function') {
const client = this.$adapter.modelConstructorClient(this, options);
return client.transaction(callbackOrOptions, options);
}
const client = this.$adapter.modelConstructorClient(this, callbackOrOptions);
return client.transaction(callbackOrOptions);
}
/**
* Create a model instance from the adapter result. The result value must
* be a valid object, otherwise `null` is returned.
*/
static $createFromAdapterResult(adapterResult, sideloadAttributes, options) {
if (typeof adapterResult !== 'object' || Array.isArray(adapterResult)) {
return null;
}
const instance = new this();
instance.$consumeAdapterResult(adapterResult, sideloadAttributes);
instance.$hydrateOriginals();
instance.$setOptionsAndTrx(options);
instance.$isPersisted = true;
instance.$isLocal = false;
return instance;
}
/**
* Creates an array of models from the adapter results. The `adapterResults`
* must be an array with valid Javascript objects.
*
* 1. If top level value is not an array, then an empty array is returned.
* 2. If row is not an object, then it will be ignored.
*/
static $createMultipleFromAdapterResult(adapterResults, sideloadAttributes, options) {
if (!Array.isArray(adapterResults)) {
return [];
}
return adapterResults.reduce((models, row) => {
if (isObject(row)) {
models.push(this.$createFromAdapterResult(row, sideloadAttributes, options));
}
return models;
}, []);
}
/**
* Define a new column on the model. This is required, so that
* we differentiate between plain properties vs model attributes.
*/
static $addColumn(name, options) {
const descriptor = Object.getOwnPropertyDescriptor(this.prototype, name);
const column = {
isPrimary: options.isPrimary || false,
columnName: options.columnName || this.namingStrategy.columnName(this, name),
hasGetter: !!(descriptor && descriptor.get),
hasSetter: !!(descriptor && descriptor.set),
serializeAs: options.serializeAs !== undefined
? options.serializeAs
: this.namingStrategy.serializedName(this, name),
serialize: options.serialize,
prepare: options.prepare,
consume: options.consume,
meta: options.meta,
};
/**
* Set column as the primary column, when `primary` is true
*/
if (column.isPrimary) {
this.primaryKey = name;
}
this.$columnsDefinitions.set(name, column);
this.$keys.attributesToColumns.add(name, column.columnName);
column.serializeAs && this.$keys.attributesToSerialized.add(name, column.serializeAs);
this.$keys.columnsToAttributes.add(column.columnName, name);
column.serializeAs && this.$keys.columnsToSerialized.add(column.columnName, column.serializeAs);
column.serializeAs && this.$keys.serializedToAttributes.add(column.serializeAs, name);
column.serializeAs && this.$keys.serializedToColumns.add(column.serializeAs, column.columnName);
return column;
}
/**
* Returns a boolean telling if column exists on the model
*/
static $hasColumn(name) {
return this.$columnsDefinitions.has(name);
}
/**
* Returns the column for a given name
*/
static $getColumn(name) {
return this.$columnsDefinitions.get(name);
}
/**
* Adds a computed node
*/
static $addComputed(name, options) {
const computed = {
serializeAs: options.serializeAs !== undefined
? options.serializeAs
: this.namingStrategy.serializedName(this, name),
meta: options.meta,
};
this.$computedDefinitions.set(name, computed);
return computed;
}
/**
* Find if some property is marked as computed
*/
static $hasComputed(name) {
return this.$computedDefinitions.has(name);
}
/**
* Get computed node
*/
static $getComputed(name) {
return this.$computedDefinitions.get(name);
}
/**
* Register has one relationship
*/
static $addHasOne(name, relatedModel, options) {
this.$relationsDefinitions.set(name, new HasOne(name, relatedModel, options, this));
}
/**
* Register has many relationship
*/
static $addHasMany(name, relatedModel, options) {
this.$relationsDefinitions.set(name, new HasMany(name, relatedModel, options, this));
}
/**
* Register belongs to relationship
*/
static $addBelongsTo(name, relatedModel, options) {
this.$relationsDefinitions.set(name, new BelongsTo(name, relatedModel, options, this));
}
/**
* Register many-to-many relationship
*/
static $addManyToMany(name, relatedModel, options) {
this.$relationsDefinitions.set(name, new ManyToMany(name, relatedModel, options, this));
}
/**
* Register has many through relationship
*/
static $addHasManyThrough(name, relatedModel, options) {
this.$relationsDefinitions.set(name, new HasManyThrough(name, relatedModel, options, this));
}
/**
* Adds a relationship
*/
static $addRelation(name, type, relatedModel, options) {
switch (type) {
case 'hasOne':
this.$addHasOne(name, relatedModel, options);
break;
case 'hasMany':
this.$addHasMany(name, relatedModel, options);
break;
case 'belongsTo':
this.$addBelongsTo(name, relatedModel, options);
break;
case 'manyToMany':
this.$addManyToMany(name, relatedModel, options);
break;
case 'hasManyThrough':
this.$addHasManyThrough(name, relatedModel, options);
break;
default:
throw new Error(`${type} is not a supported relation type`);
}
}
/**
* Find if some property is marked as a relation or not
*/
static $hasRelation(name) {
return this.$relationsDefinitions.has(name);
}
/**
* Returns relationship node for a given relation
*/
static $getRelation(name) {
return this.$relationsDefinitions.get(name);
}
/**
* Define a static property on the model using the inherit or
* define strategy.
*
* Inherit strategy will clone the property from the parent model
* and will set it on the current model
*/
static $defineProperty(propertyName, defaultValue, strategy) {
defineStaticProperty(this, propertyName, {
initialValue: defaultValue,
strategy: strategy,
});
}
/**
* Boot the model
*/
static boot() {
/**
* Define the property when not defined on self
*/
if (!this.hasOwnProperty('booted')) {
this.booted = false;
}
/**
* Return when already booted
*/
if (this.booted === true) {
return;
}
this.booted = true;
/**
* Table name is never inherited from the base model
*/
this.$defineProperty('table', this.namingStrategy.tableName(this), 'define');
/**
* Inherit primary key or default to "id"
*/
this.$defineProperty('primaryKey', 'id', 'inherit');
/**
* Inherit selfAssignPrimaryKey or default to "false"
*/
this.$defineProperty('selfAssignPrimaryKey', false, 'inherit');
/**
* Define the keys' property. This allows looking up variations
* for model keys
*/
this.$defineProperty('$keys', {
attributesToColumns: new ModelKeys(),
attributesToSerialized: new ModelKeys(),
columnsToAttributes: new ModelKeys(),
columnsToSerialized: new ModelKeys(),
serializedToColumns: new ModelKeys(),
serializedToAttributes: new ModelKeys(),
}, (value) => {
return {
attributesToColumns: new ModelKeys(Object.assign({}, value.attributesToColumns.all())),
attributesToSerialized: new ModelKeys(Object.assign({}, value.attributesToSerialized.all())),
columnsToAttributes: new ModelKeys(Object.assign({}, value.columnsToAttributes.all())),
columnsToSerialized: new ModelKeys(Object.assign({}, value.columnsToSerialized.all())),
serializedToColumns: new ModelKeys(Object.assign({}, value.serializedToColumns.all())),
serializedToAttributes: new ModelKeys(Object.assign({}, value.serializedToAttributes.all())),
};
});
/**
* Define columns
*/
this.$defineProperty('$columnsDefinitions', new Map(), 'inherit');
/**
* Define computed properties
*/
this.$defineProperty('$computedDefinitions', new Map(), 'inherit');
/**
* Define relationships
*/
this.$defineProperty('$relationsDefinitions', new Map(), (value) => {
const relations = new Map();
value.forEach((relation, key) => {
const relationClone = relation.clone(this);
relations.set(key, relationClone);
});
return relations;
});
/**
* Define hooks.
*/
this.$defineProperty('$hooks', new Hooks(), (value) => {
const hooks = new Hooks();
hooks.merge(value);
return hooks;
});
}
static before(event, handler) {
this.$hooks.add(`before:${event}`, handler);
}
static after(event, handler) {
this.$hooks.add(`after:${event}`, handler);
}
/**
* Returns a fresh persisted instance of model by applying
* attributes to the model instance
*/
static async create(values, options) {
const instance = this.newUpWithOptions(values, options, options?.allowExtraProperties);
await instance.save();
return instance;
}
/**
* Same as [[BaseModel.create]] without invoking hooks.
*/
static async createQuietly(values, options) {
const instance = this.newUpWithOptions(values, options, options?.allowExtraProperties);
await instance.saveQuietly();
return instance;
}
/**
* Same as [[BaseModel.create]], but persists multiple instances. The create
* many call will be wrapped inside a managed transaction for consistency.
* If required, you can also pass a transaction client and the method
* will use that instead of create a new one.
*/
static async createMany(values, options) {
const client = this.$adapter.modelConstructorClient(this, options);
return managedTransaction(client, async (trx) => {
const modelInstances = [];
const createOptions = {
client: trx,
allowExtraProperties: options?.allowExtraProperties,
};
for (let row of values) {
const modelInstance = await this.create(row, createOptions);
modelInstances.push(modelInstance);
}
return modelInstances;
});
}
/**
* Same as [[BaseModel.createMany]] without invoking hooks.
*/
static async createManyQuietly(values, options) {
const client = this.$adapter.modelConstructorClient(this, options);
return managedTransaction(client, async (trx) => {
const modelInstances = [];
const createOptions = {
client: trx,
allowExtraProperties: options?.allowExtraProperties,
};
for (let row of values) {
const modelInstance = await this.createQuietly(row, createOptions);
modelInstances.push(modelInstance);
}
return modelInstances;
});
}
/**
* Find model instance using the primary key
*/
static async find(value, options) {
if (value === undefined) {
throw new Exception('"find" expects a value. Received undefined');
}
return this.findBy(this.primaryKey, value, options);
}
/**
* Find model instance using the primary key
*/
static async findOrFail(value, options) {
if (value === undefined) {
throw new Exception('"findOrFail" expects a value. Received undefined');
}
return this.findByOrFail(this.primaryKey, value, options);
}
static async findBy(key, value, options) {
if (typeof key === 'object') {
return this.query(value)
.where(key)
.first();
}
if (value === undefined) {
throw new Exception('"findBy" expects a value. Received undefined');
}
return this.query(options).where(key, value).first();
}
static async findByOrFail(key, value, options) {
if (typeof key === 'object') {
return this.query(value)
.where(key)
.firstOrFail();
}
if (value === undefined) {
throw new Exception('"findByOrFail" expects a value. Received undefined');
}
return this.query(options).where(key, value).firstOrFail();
}
static findManyBy(key, value, options) {
if (typeof key === 'object') {
return this.query(value)
.where(key)
.exec();
}
if (value === undefined) {
throw new Exception('"findManyBy" expects a value. Received undefined');
}
return this.query(options).where(key, value).exec();
}
/**
* Same as `query().first()`
*/
static async first(options) {
return this.query(options).first();
}
/**
* Same as `query().firstOrFail()`
*/
static async firstOrFail(options) {
return this.query(options).firstOrFail();
}
/**
* Find model instance using a key/value pair
*/
static async findMany(value, options) {
if (value === undefined) {
throw new Exception('"findMany" expects a value. Received undefined');
}
return this.query(options)
.whereIn(this.primaryKey, value)
.orderBy(this.primaryKey, 'desc')
.exec();
}
/**
* Find model instance using a key/value pair or create a
* new one without persisting it.
*/
static async firstOrNew(searchPayload, savePayload, options) {
/**
* Search using the search payload and fetch the first row
*/
const query = this.query(options).where(searchPayload);
const row = await query.first();
/**
* Create a new one, if row is not found
*/
if (!row) {
return this.newUpWithOptions(Object.assign({}, searchPayload, savePayload), query.clientOptions, options?.allowExtraProperties);
}
return row;
}
/**
* Same as `firstOrNew`, but also persists the newly created model instance.
*/
static async firstOrCreate(searchPayload, savePayload, options) {
/**
* Search using the search payload and fetch the first row
*/
const query = this.query(options).where(searchPayload);
let row = await query.first();
/**
* Create a new instance and persist it to the database
*/
if (!row) {
row = this.newUpWithOptions(Object.assign({}, searchPayload, savePayload), query.clientOptions, options?.allowExtraProperties);
await row.save();
}
return row;
}
/**
* Updates or creates a new row inside the database
*/
static async updateOrCreate(searchPayload, updatedPayload, options) {
const client = this.$adapter.modelConstructorClient(this, options);
/**
* We wrap updateOrCreate call inside a transaction and obtain an update
* lock on the selected row. This ensures that concurrent reads waits
* for the existing writes to finish
*/
return managedTransaction(client, async (trx) => {
const query = this.query({ client: trx }).forUpdate().where(searchPayload);
let row = await query.first();
/**
* Create a new instance or update the existing one (if found)
*/
if (!row) {
row = this.newUpWithOptions(Object.assign({}, searchPayload, updatedPayload), query.clientOptions, options?.allowExtraProperties);
}
else {
row.merge(updatedPayload, options?.allowExtraProperties);
}
await row.save();
return row;
});
}
/**
* Find existing rows or create an in-memory instances of the missing ones.
*/
static async fetchOrNewUpMany(uniqueKeys, payload, options) {
const client = this.$adapter.modelConstructorClient(this, options);
uniqueKeys = Array.isArray(uniqueKeys) ? uniqueKeys : [uniqueKeys];
const uniquenessPair = uniqueKeys.map((uniqueKey) => {
return {
key: uniqueKey,
value: collectValues(payload, uniqueKey, () => {
throw new Exception(`Value for the "${uniqueKey}" is null or undefined inside "fetchOrNewUpMany" payload`);
}).map((value) => transformDateValue(value, client.dialect)),
};
});
/**
* Find existing rows
*/
const query = this.query(options);
uniquenessPair.forEach(({ key, value }) => query.whereIn(key, value));
const existingRows = await query;
/**
* Return existing rows as it is and create a model instance for missing one's
*/
return this.newUpIfMissing(payload, existingRows, uniqueKeys, false, query.clientOptions, options?.allowExtraProperties);
}
/**
* Find existing rows or create missing one's. One database call per insert
* is invoked, so that each insert goes through the lifecycle of model
* hooks.
*/
static async fetchOrCreateMany(uniqueKeys, payload, options) {
const client = this.$adapter.modelConstructorClient(this, options);
uniqueKeys = Array.isArray(uniqueKeys) ? uniqueKeys : [uniqueKeys];
const uniquenessPair = uniqueKeys.map((uniqueKey) => {
return {
key: uniqueKey,
value: collectValues(payload, uniqueKey, () => {
throw new Exception(`Value for the "${uniqueKey}" is null or undefined inside "fetchOrCreateMany" payload`);
}).map((value) => transformDateValue(value, client.dialect)),
};
});
/**
* Find existing rows
*/
const query = this.query(options);
uniquenessPair.forEach(({ key, value }) => query.whereIn(key, value));
const existingRows = await query;
/**
* Create model instance for the missing rows
*/
const rows = this.newUpIfMissing(payload, existingRows, uniqueKeys, false, query.clientOptions, options?.allowExtraProperties);
/**
* Persist inside db inside a transaction
*/
await managedTransaction(query.client, async (trx) => {
for (let row of rows) {
/**
* If transaction `client` was passed, then the row will have
* the `trx` already set. But since, the trx of row will be
* same as the `trx` passed to this callback, we can safely
* re-set it.
*/
row.$trx = trx;
if (!row.$isPersisted) {
await row.save();
}
}
});
return rows;
}
/**
* Update existing rows or create missing one's. One database call per insert
* is invoked, so that each insert and update goes through the lifecycle
* of model hooks.
*/
static async updateOrCreateMany(uniqueKeys, payload, options) {
const client = this.$adapter.modelConstructorClient(this, options);
uniqueKeys = Array.isArray(uniqueKeys) ? uniqueKeys : [uniqueKeys];
const uniquenessPair = uniqueKeys.map((uniqueKey) => {
return {
key: uniqueKey,
value: collectValues(payload, uniqueKey, () => {
throw new Exception(`Value for the "${uniqueKey}" is null or undefined inside "updateOrCreateMany" payload`);
}).map((value) => transformDateValue(value, client.dialect)),
};
});
return managedTransaction(client, async (trx) => {
/**
* Find existing rows
*/
const query = this.query({ client: trx }).forUpdate();
uniquenessPair.forEach(({ key, value }) => query.whereIn(key, value));
const existingRows = await query;
/**
* Create model instance for the missing rows
*/
const rows = this.newUpIfMissing(payload, existingRows, uniqueKeys, true, query.clientOptions, options?.allowExtraProperties);
for (let row of rows) {
await row.save();
}
return rows;
});
}
/**
* Returns all rows from the model table
*/
static async all(options) {
return this.query(options).orderBy(this.primaryKey, 'desc');
}
/**
* Truncate model table
*/
static truncate(cascade = false) {
return this.query().client.truncate(this.table, cascade);
}
constructor() {
return new Proxy(this, proxyHandler);
}
/**
* Custom options defined on the model instance that are
* passed to the adapter
*/
modelOptions;
/**
* Reference to transaction that will be used for performing queries on a given
* model instance.
*/
modelTrx;
/**
* The transaction listener listens for the `commit` and `rollback` events and
* cleanup the `$trx` reference
*/
transactionListener = function listener() {
this.modelTrx = undefined;
}.bind(this);
/**
* When `fill` method is called, then we may have a situation where it
* removed the values which exists in `original` and hence the dirty
* diff has to do a negative diff as well
*/
fillInvoked = false;
/**
* A copy of cached getters
*/
cachedGetters = {};
/**
* Find if force updates are enabled
*/
forceUpdate = false;
/**
* Raises exception when mutations are performed on a delete model
*/
ensureIsntDeleted() {
if (this.$isDeleted) {
throw new errors.E_MODEL_DELETED();
}
}
/**
* Invoked when performing the insert call. The method initiates
* all `datetime` columns, if there are not initiated already
* and `autoCreate` or `autoUpdate` flags are turned on.
*/
initiateAutoCreateColumns() {
const model = this.constructor;
model.$columnsDefinitions.forEach((column, attributeName) => {
const columnType = column.meta?.type;
/**
* Return early when not dealing with date time columns
*/
if (!columnType || !(columnType in DATE_TIME_TYPES)) {
return;
}
/**
* Set the value when its missing and `autoCreate` or `autoUpdate`
* flags are defined.
*/
const attributeValue = this[attributeName];
if (!attributeValue && (column.meta.autoCreate || column.meta.autoUpdate)) {
;
this[attributeName] = DateTime.local();
return;
}
});
}
/**
* Invoked when performing the update call. The method initiates
* all `datetime` columns, if there have `autoUpdate` flag
* turned on.
*/
initiateAutoUpdateColumns() {
const model = this.constructor;
model.$columnsDefinitions.forEach((column, attributeName) => {
const columnType = column.meta?.type;
/**
* Return early when not dealing with date time columns or auto update
* is not set to true
*/
if (!columnType || !(columnType in DATE_TIME_TYPES) || !column.meta.autoUpdate) {
return;
}
;
this[attributeName] = DateTime.local();
});
}
/**
* Preparing the object to be sent to the adapter. We need
* to create the object with the property names to be
* used by the adapter.
*/
prepareForAdapter(attributes) {
const Model = this.constructor;
return Object.keys(attributes).reduce((result, key) => {
const column = Model.$getColumn(key);
const value = typeof column.prepare === 'function'
? column.prepare(attributes[key], key, this)
: attributes[key];
result[column.columnName] = value;
return result;
}, {});
}
/**
* Returns true when the field must be included
* inside the serialized object.
*/
shouldSerializeField(serializeAs, fields) {
/**
* If explicit serializing is turned off, then never
* return the field
*/
if (!serializeAs) {
return false;
}
/**
* If not explicit fields are defined, then always include the field
*/
if (!fields) {
return true;
}
const { pick, omit } = normalizeCherryPickObject(fields);
/**
* Return false, when under omit array
*/
if (omit && omit.includes(serializeAs)) {
return false;
}
/**
* Otherwise ensure is inside pick array
*/
return !pick || pick.includes(serializeAs);
}
/**
* A type only reference to the columns
*/
$columns = {};
/**
* A copy of attributes that will be sent over to adapter
*/
$attributes = {};
/**
* Original represents the properties that already has been
* persisted or loaded by the adapter.
*/
$original = {};
/**
* Preloaded relationships on the model instance
*/
$preloaded = {};
/**
* Extras are dynamic properties set on the model instance, which
* are not serialized and neither cast for adapter calls.
*
* This is helpful when adapter wants to load some extra data conditionally
* and that data must not be persisted back the adapter.
*/
$extras = {};
/**
* Side-loaded are dynamic properties set on the model instance, which
* are not serialized and neither cast for adapter calls.
*
* This is helpful when you want to add dynamic metadata to the model
* and it's children as well.
*
* The difference between [[extras]] and [[sideloaded]] is:
*
* - Extras can be different for each model instance
* - Extras are not shared down the hierarchy (example relationships)
* - Side-loaded are shared across multiple model instances created via `$createMultipleFromAdapterResult`.
* - Side-loaded are passed to the relationships as well.
*/
$sideloaded = {};
/**
* Persisted means the model has been persisted with the adapter. This will
* also be true, when model instance is created as a result of fetch
* call from the adapter.
*/
$isPersisted = false;
/**
* Once deleted the model instance cannot make calls to the adapter
*/
$isDeleted = false;
/**
* `$isLocal` tells if the model instance was created locally vs
* one generated as a result of fetch call from the adapter.
*/
$isLocal = true;
/**
* Returns the value of primary key. The value must be
* set inside attributes object
*/
get $primaryKeyValue() {
const model = this.constructor;
const column = model.$getColumn(model.primaryKey);
if (column && column.hasGetter) {
return this[model.primaryKey];
}
return this.$getAttribute(model.primaryKey);
}
/**
* Opposite of [[this.isPersisted]]
*/
get $isNew() {
return !this.$isPersisted;
}
/**
* Returns dirty properties of a model by doing a diff
* between original values and current attributes
*/
get $dirty() {
const processedKeys = [];
/**
* Do not compute diff, when model has never been persisted
*/
if (!this.$isPersisted) {
return this.$attributes;
}
const dirty = Object.keys(this.$attributes).reduce((result, key) => {
const value = this.$attributes[key];
const originalValue = this.$original[key];
let isEqual = true;
if (isObject(value) && 'isDirty' in value) {
isEqual = !value.isDirty;
}
else {
isEqual = compareValues(originalValue, value);
}
if (!isEqual) {
result[key] = value;
}
if (this.fillInvoked) {
processedKeys.push(key);
}
return result;
}, {});
/**
* Find negative diff if fill was invoked, since we may have removed values
* that exists in originals
*/
if (this.fillInvoked) {
Object.keys(this.$original)
.filter((key) => !processedKeys.includes(key))
.forEach((key) => {
dirty[key] = null;
});
}
return dirty;
}
/**
* Finding if model is dirty with changes or not
*/
get $isDirty() {
return Object.keys(this.$dirty).length > 0;
}
/**
* Returns the transaction
*/
get $trx() {
return this.modelTrx;
}
/**
* Set the trx to be used by the model to executing queries
*/
set $trx(trx) {
if (!trx) {
this.modelTrx = undefined;
return;
}
/**
* Remove old listeners
*/
if (this.modelTrx) {
this.modelTrx.removeListener('commit', this.transactionListener);
this.modelTrx.removeListener('rollback', this.transactionListener);
}
/**
* Store reference to the transaction
*/
this.modelTrx = trx;
this.modelTrx.once('commit', this.transactionListener);
this.modelTrx.once('rollback', this.transactionListener);
}
/**
* Get options
*/
get $options() {
return this.modelOptions;
}
/**
* Set options
*/
set $options(options) {
if (!options) {
this.modelOptions = undefined;
return;
}
this.modelOptions = this.modelOptions || {};
if (options.connection) {
this.modelOptions.connection = options.connection;
}
}
/**
* Set options on the model instance along with transaction
*/
$setOptionsAndTrx(options) {
if (!options) {
return;
}
if (options.client && options.client.isTransaction) {
this.$trx = options.client;
}
this.$options = options;
}
/**
* A chainable method to set transaction on the model
*/
useTransaction(trx) {
this.$trx = trx;
return this;
}
/**
* A chainable method to set transaction on the model
*/
useConnection(connection) {
this.$options = { connection };
return this;
}
/**
* Set attribute
*/
$setAttribute(key, value) {
this.ensureIsntDeleted();
this.$attributes[key] = value;
}
/**
* Get value of attribute
*/
$getAttribute(key) {
return this.$attributes[key];
}
/**
* Returns the attribute value from the cache which was resolved by
* the mutated by a getter. This is done to avoid re-mutating
* the same attribute value over and over again.
*/
$getAttributeFromCache(key, callback) {
const original = this.$getAttribute(key);
const cached = this.cachedGetters[key];
/**
* Return the resolved value from cache when cache original is same
* as the attribute value
*/
if (cached && cached.original === original) {
return cached.resolved;
}
/**
* Re-resolve the value from the callback
*/
const resolved = callback(original);
if (!cached) {
/**
* Create cache entry
*/
this.cachedGetters[key] = { getter: callback, original, resolved };
}
else {
/**
* Update original and resolved keys
*/
this.cachedGetters[key].original = original;
this.cachedGetters[key].resolved = resolved;
}
return resolved;
}
/**
* Returns the related model or default value when model is missing
*/
$getRelated(key) {
return this.$preloaded[key];
}
/**
* A boolean to know if relationship has been preloaded or not
*/
$hasRelated(key) {
return this.$preloaded[key] !== undefined;
}
/**
* Sets the related data on the model instance. The method internally handles
* `one to one` or `many` relations
*/
$setRelated(key, models) {
const Model = this.constructor;
const relation = Model.$relationsDefinitions.get(key);
/**
* Ignore when relation is not defined
*/
if (!relation) {
return;
}
/**
* Reset array before invoking $pushRelated
*/
if (MANY_RELATIONS.includes(relation.type)) {
if (!Array.isArray(models)) {
throw new Exception(`"${Model.name}.${key}" must be an array when setting "${relation.type}" relationship`);
}
this.$preloaded[key] = [];
}
return this.$pushRelated(key, models);
}
/**
* Push related adds to the existing related collection
*/
$pushRelated(key, models) {
const Model = this.constructor;
const relation = Model.$relationsDefinitions.get(key);
/**
* Ignore when relation is not defined
*/
if (!relation) {
return;
}
/**
* Create multiple for `hasMany` `manyToMany` and `hasManyThrough`
*/
if (MANY_RELATIONS.includes(relation.type)) {
this.$preloaded[key] = (this.$preloaded[key] || []).concat(models);
return;
}
/**
* Dis-allow setting multiple model instances for a one-to-one relationship
*/
if (Array.isArray(models)) {
throw new Error(`"${Model.name}.${key}" cannot reference more than one instance of "${relation.relatedModel().name}" model`);
}
this.$preloaded[key] = models;
}
/**
* Merges the object with the model attributes, assuming object keys
* are coming the database.
*
* 1. If key is unknown, it will be added to the `extras` object.
* 2. If key is defined as a relationship, it will be ignored and one must call `$setRelated`.
*/
$consumeAdapterResult(adapterResult, sideloadedAttributes) {
const Model = this.constructor;
/**
* Merging sideloaded attributes with the existing sideloaded values
* on the model instance
*/
if (sideloadedAttributes) {
this.$sideloaded = Object.assign({}, this.$sideloaded, sideloadedAttributes);
}
/**
* Merge result of adapter with the attributes. This enables
* the adapter to hydrate models with properties generated
* as a result of insert or update
*/
if (isObject(adapterResult)) {
Object.keys(adapterResult).forEach((key) => {
/**
* Pull the attribute name from the column name, since adapter
* results always holds the column names.
*/
const attributeName = Model.$keys.columnsToAttributes.get(key);
if (attributeName) {
const attribute = Model.$getColumn(attributeName);
/**
* Invoke `consume` method for the column, before setting the
* attribute
*/
const value = typeof attribute.consume === 'function'
? attribute.consume(adapterResult[key], attributeName, this)
: adapterResult[key];
/**
* When consuming the adapter result, we must always set the attributes
* directly, as we do not want to invoke setters.
*/
this.$setAttribute(attributeName, value);
return;
}
/**
* If key is defined as a relation, then ignore it, since one
* must pass a qualified model to `this.$setRelated()`
*/
if (Model.$relationsDefinitions.has(key)) {
return;
}
/**
* Set directly on the model
*/
if (this.hasOwnProperty(key)) {
;
this[key] = adapterResult[key];
return;
}
this.$extras[key] = adapterResult[key];
});
}
}
/**
* Sync originals with the attributes. After this `isDirty` will
* return false
*/
$hydrateOriginals() {
this.$original = {};
lodash.merge(this.$original, this.$attributes);
}
/**
* Set bulk attributes on the model instance. Setting relationships via
* fill isn't allowed, since we disallow setting relationships
* locally
*/
fill(values, allowExtraProperties = false) {
this.$attributes = {};
this.merge(values, allowExtraProperties);
this.fillInvoked = true;
return this;
}
/**
* Merge bulk attributes with existing attributes.
*
* 1. If key is unknown, it will be added to the `extras` object.
* 2. If key is defined as a relationship, it will be ignored and one must call `$setRelated`.
*/
merge(values, allowExtraProperties = false) {
const Model = this.constructor;
/**
* Merge values with the attributes
*/
if (isObject(values)) {
Object.keys(values).forEach((key) => {
const value = values[key];
/**
* Set as column
*/
if (Model.$hasColumn(key)) {
;
this[key] = value;
return;
}
/**
* Resolve the attribute name from the column names. Since people
* usually define the column names directly as well by
* accepting them directly from the API.
*/
const attributeName = Model.$keys.columnsToAttributes.get(key);
if (attributeName) {
;
this[attributeName] = value;
return;
}
/**
* If key is defined as a relation, then ignore it, since one
* must pass a qualified model to `this.$setRelated()`
*/
if (Model.$relationsDefinitions.has(key)) {
return;
}
/**
* If the property already exists on the model, then set it
* as it is vs defining it as an extra property
*/
if (this.hasOwnProperty(key)) {
;
this[key] = value;
return;
}
/**
* Raise error when not instructed to ignore non-existing properties.
*/
if (!allowExtraProperties) {
throw new Error(`Cannot define "${key}" on "${Model.name}" model, since it is not defined as a model property`);
}
this.$extras[key] = value;
});
}
return this;
}
/**
* Returns whether any of the fields have been modified
*/
isDirty(fields) {
const keys = Array.isArray(fields) ? fields : fields ? [fields] : [];
if (keys.length === 0) {
return this.$isDirty;
}
return keys.some((key) => key in this.$dirty);
}
/**
* Enable force update even when no attributes
* are dirty
*/
enableForceUpdate() {
this.forceUpdate = true;
return this;
}
/**
* Preloads one or more relationships for the current model
*/
async load(relationName, callback) {
this.ensureIsntDeleted();
if (!this.$isPersisted) {
throw new Exception('Cannot lazy load relationship for an unpersisted model instance');
}
const Model = this.constructor;
const preloader = new Preloader(Model);
if (typeof relationName === 'function') {
relationName(preloader);
}
else {
preloader.load(relationName, callback);
}
const queryClient = Model.$adapter.modelClient(this);
await preloader
.sideload(this.$sideloaded)
.debug(queryClient.debug)
.processAllForOne(this, queryClient);
}
/**
* Load relationships onto the instance, but only if they are not
* already preloaded
*/
async loadOnce(relationName) {
if (!this.$preloaded[relationName]) {
return this.load(relationName);
}
}
/**
* @deprecated
*/
async preload(relationName, callback) {
process.emitWarning('DeprecationWarning', '"Model.preload()" is deprecated. Use "Model.load()" instead');
return this.load(relationName, callback);
}
/**
* Lazy load the relationship aggregate value
*/
loadAggregate(relationName, callback) {
this.ensureIsntDeleted();
if (!this.$isPersisted) {
throw new Exception('Cannot lazy load relationship aggregates for an unpersisted model instance');
}
return new LazyLoadAggregates(this).loadAggregate(relationName, callback);
}
/**
* Lazy load the relationship count value
*/
loadCount(relationName, callback) {
this.ensureIsntDeleted();
if (!this.$isPersisted) {
throw new Exception('Cannot lazy load relationship aggregates for an unpersisted model instance');
}
return new LazyLoadAggregates(this).loadCount(relationName, callback);
}
/**
* Perform save on the model instance to commit mutations.
*/
async save() {
this.ensureIsntDeleted();
const Model