pinia-orm
Version:
The Pinia plugin to enable Object-Relational Mapping access to the Pinia Store.
1,744 lines (1,728 loc) • 101 kB
JavaScript
import { i as isArray, t as throwError, a as assert, g as generateId, b as isNullish, c as compareWithOperator, d as generateKey, e as isEmpty, f as isFunction, h as groupBy, o as orderBy, j as equals, k as isDate } from './shared/pinia-orm.DGc38JnV.mjs';
import { defineStore, acceptHMRUpdate } from 'pinia';
import { schema, normalize } from '@pinia-orm/normalizr';
import { ref } from 'vue-demi';
export { C as CastAttribute } from './shared/pinia-orm.C7bM_uXu.mjs';
class Attribute {
/**
* The model instance.
*/
model;
/**
* The field name
*/
key;
/**
* Create a new Attribute instance.
*/
constructor(model) {
this.model = model;
this.key = "";
}
/**
* Set the key name of the field
*/
setKey(key) {
this.key = key;
return this;
}
}
class Relation extends Attribute {
/**
* The parent model.
*/
parent;
/**
* The related model.
*/
related;
/**
* The delete mode
*/
onDeleteMode;
/**
* Create a new relation instance.
*/
constructor(parent, related) {
super(parent);
this.parent = parent;
this.related = related;
}
/**
* Get the related model of the relation.
*/
getRelated() {
return this.related;
}
/**
* Get all of the primary keys for an array of models.
*/
getKeys(models, key) {
return models.map((model) => model[key]);
}
/**
* Specify how this model should behave on delete
*/
onDelete(mode) {
this.onDeleteMode = mode;
return this;
}
/**
* Run a dictionary map over the items.
*/
mapToDictionary(models, callback) {
return models.reduce((dictionary, model) => {
const [key, value] = callback(model);
if (!dictionary[key]) {
dictionary[key] = [];
}
dictionary[key].push(value);
return dictionary;
}, {});
}
/**
* Call a function for a current key match
*/
compositeKeyMapper(foreignKey, localKey, call) {
if (isArray(foreignKey) && isArray(localKey)) {
foreignKey.forEach((key, index) => {
call(key, localKey[index]);
});
} else if (!isArray(localKey) && !isArray(foreignKey)) {
call(foreignKey, localKey);
} else {
throwError([
"This relation cant be resolve. Either child or parent doesnt have different key types (composite)",
JSON.stringify(foreignKey),
JSON.stringify(localKey)
]);
}
}
/**
* Get the index key defined by the primary key or keys (composite)
*/
getResolvedKey(model, key) {
return isArray(key) ? `[${key.map((keyPart) => model[keyPart]).toString()}]` : model[key];
}
}
class MorphTo extends Relation {
/**
* The related models.
*/
relatedModels;
/**
* The related model dictionary.
*/
relatedTypes;
/**
* The field name that contains id of the parent model.
*/
morphId;
/**
* The field name that contains type of the parent model.
*/
morphType;
/**
* The associated key of the child model.
*/
ownerKey;
/**
* Create a new morph-to relation instance.
*/
constructor(parent, relatedModels, morphId, morphType, ownerKey) {
super(parent, parent);
this.relatedModels = relatedModels;
this.relatedTypes = this.createRelatedTypes(relatedModels);
this.morphId = morphId;
this.morphType = morphType;
this.ownerKey = ownerKey;
}
/**
* Create a dictionary of relations keyed by their entity.
*/
createRelatedTypes(models) {
return models.reduce((types, model) => {
types[model.$entity()] = model;
return types;
}, {});
}
/**
* Get the type field name.
*/
getType() {
return this.morphType;
}
/**
* Get all related models for the relationship.
*/
getRelateds() {
return this.relatedModels;
}
/**
* Define the normalizr schema for the relation.
*/
define(schema) {
return schema.union(this.relatedModels, (value, parent, _key) => {
const type = parent[this.morphType];
const model = this.relatedTypes[type];
const key = this.ownerKey || model.$getKeyName();
parent[this.morphId] = value[key];
return type;
});
}
/**
* Attach the relational key to the given record. Since morph-to relationship
* doesn't have any foreign key, it would do nothing.
*/
attach(_record, _child) {
}
/**
* Add eager constraints. Since we do not know the related model ahead of time,
* we cannot add any eager constraints.
*/
addEagerConstraints(_query, _models) {
}
/**
* Find and attach related children to their respective parents.
*/
match(relation, models, query) {
const dictionary = this.buildDictionary(query, models);
models.forEach((model) => {
const type = model[this.morphType];
const id = model[this.morphId];
const related = dictionary[type]?.[id] ?? null;
model.$setRelation(relation, related);
});
}
/**
* Make a related model.
*/
make(element, type) {
if (!element || !type) {
return null;
}
return this.relatedTypes[type].$newInstance(element);
}
/**
* Build model dictionary keyed by the owner key for each entity.
*/
buildDictionary(query, models) {
const keys = this.getKeysByEntity(models);
const dictionary = {};
for (const entity in keys) {
const model = this.relatedTypes[entity];
assert(!!model, [
`Trying to load "morph to" relation of \`${entity}\``,
"but the model could not be found."
]);
const ownerKey = this.ownerKey || model.$getKeyName();
const results = query.newQueryWithConstraints(entity).whereIn(ownerKey, keys[entity]).get(false);
dictionary[entity] = results.reduce(
(dic, result) => {
dic[result[ownerKey]] = result;
return dic;
},
{}
);
}
return dictionary;
}
/**
* Get the relation's primary keys grouped by its entity.
*/
getKeysByEntity(models) {
return models.reduce((keys, model) => {
const type = model[this.morphType];
const id = model[this.morphId];
if (id !== null && this.relatedTypes[type] !== void 0) {
if (!keys[type]) {
keys[type] = [];
}
keys[type].push(id);
}
return keys;
}, {});
}
}
class Type extends Attribute {
/**
* The raw default value for the attribute (can be a function).
*/
rawDefaultValue;
/**
* Whether the attribute accepts `null` value or not.
*/
isNullable = true;
/**
* Create a new Type attribute instance.
*/
constructor(model, defaultValue = null) {
super(model);
this.rawDefaultValue = defaultValue;
}
/**
* The computed default value of the attribute.
*/
get defaultValue() {
return typeof this.rawDefaultValue === "function" ? this.rawDefaultValue() : this.rawDefaultValue;
}
/**
* Set the nullable option to false.
*/
notNullable() {
this.isNullable = false;
return this;
}
makeReturn(type, value) {
if (value === void 0) {
return this.defaultValue;
}
if (value === null) {
if (!this.isNullable) {
this.throwWarning(["is set as non nullable!"]);
}
return value;
}
if (typeof value !== type) {
this.throwWarning([value, "is not a", type]);
}
return value;
}
/**
* Throw warning for wrong type
*/
throwWarning(message) {
console.warn(["[Pinia ORM]"].concat([`Field ${this.model.$entity()}:${this.key} - `, ...message]).join(" "));
}
}
class Uid extends Type {
options;
// This alphabet uses `A-Za-z0-9_-` symbols.
// The order of characters is optimized for better gzip and brotli compression.
// References to the same file (works both for gzip and brotli):
// `'use`, `andom`, and `rict'`
// References to the brotli default dictionary:
// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf`
alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
size = 21;
constructor(model, options = {}) {
super(model);
this.options = typeof options === "number" ? { size: options } : options;
this.alphabet = this.options.alphabet ?? this.alphabet;
this.size = this.options.size ?? this.size;
}
/**
* Make the value for the attribute.
*/
make(value) {
const uidCast = this.model.$casts()[this.model.$getKeyName()];
if (uidCast) {
return value ?? uidCast.withParameters(this.options).newRawInstance(this.model.$fields()).set(value);
}
return value ?? generateId(this.size, this.alphabet);
}
}
class Schema {
/**
* The list of generated schemas.
*/
schemas = {};
/**
* The model instance.
*/
model;
/**
* Create a new Schema instance.
*/
constructor(model) {
this.model = model;
}
/**
* Create a single schema.
*/
one(model, parent) {
model = model || this.model;
parent = parent || this.model;
const entity = `${model.$self().modelEntity()}${parent.$self().modelEntity()}`;
if (this.schemas[entity]) {
return this.schemas[entity];
}
const schema = this.newEntity(model, parent);
this.schemas[entity] = schema;
const definition = this.definition(model);
schema.define(definition);
return schema;
}
/**
* Create an array schema for the given model.
*/
many(model, parent) {
return new schema.Array(this.one(model, parent));
}
/**
* Create an union schema for the given models.
*/
union(models, callback) {
const schemas = models.reduce((schemas2, model) => {
schemas2[model.$self().modelEntity()] = this.one(model);
return schemas2;
}, {});
return new schema.Union(schemas, callback);
}
/**
* Create a new normalizr entity.
*/
newEntity(model, parent) {
const entity = model.$self().modelEntity();
const idAttribute = this.idAttribute(model, parent);
return new schema.Entity(entity, {}, { idAttribute });
}
/**
* The `id` attribute option for the normalizr entity.
*
* Generates any missing primary keys declared by a Uid attribute. Missing
* primary keys where the designated attributes do not exist will
* throw an error.
*
* Note that this will only generate uids for primary key attributes since it
* is required to generate the "index id" while the other attributes are not.
*
* It's especially important when attempting to "update" records since we'll
* want to retain the missing attributes in-place to prevent them being
* overridden by newly generated uid values.
*
* If uid primary keys are omitted, when invoking the "update" method, it will
* fail because the uid values will never exist in the store.
*
* While it would be nice to throw an error in such a case, instead of
* silently failing an update, we don't have a way to detect whether users
* are trying to "update" records or "inserting" new records at this stage.
* Something to consider for future revisions.
*/
idAttribute(model, parent) {
const uidFields = this.getUidPrimaryKeyPairs(model);
return (record, parentRecord, key) => {
if (key !== null) {
parent.$fields()[key]?.attach(parentRecord, record);
}
for (const key2 in uidFields) {
if (isNullish(record[key2])) {
record[key2] = uidFields[key2].setKey(key2).make(record[key2]);
}
}
if (["BelongsTo", "HasOne", "MorphOne", "MorphTo"].includes(parent.$fields()[key]?.constructor.name ?? "") && isArray(parentRecord[key])) {
throwError(['You are passing a list to "', `${parent.$modelEntity()}.${key}`, `" which is a one to one Relation(${parent.$fields()[key]?.constructor.name}):`, JSON.stringify(parentRecord[key])]);
}
const id = model.$getIndexId(record);
return id;
};
}
/**
* Get all primary keys defined by the Uid attribute for the given model.
*/
getUidPrimaryKeyPairs(model) {
const fields = model.$fields();
const key = model.$getKeyName();
const keys = isArray(key) ? key : [key];
const attributes = {};
keys.forEach((k) => {
const attr = fields[k];
if (attr instanceof Uid) {
attributes[k] = attr;
}
});
return attributes;
}
/**
* Create a definition for the given model.
*/
definition(model) {
const fields = model.$fields();
const definition = {};
for (const key in fields) {
const field = fields[key];
if (field instanceof Relation) {
definition[key] = field.define(this);
}
}
return definition;
}
}
class Interpreter {
/**
* The model object.
*/
model;
/**
* Create a new Interpreter instance.
*/
constructor(model) {
this.model = model;
}
process(data) {
const normalizedData = this.normalize(data);
return [data, normalizedData];
}
/**
* Normalize the given data.
*/
normalize(data) {
const schema = isArray(data) ? [this.getSchema()] : this.getSchema();
return normalize(data, schema).entities;
}
/**
* Get the schema from the database.
*/
getSchema() {
return new Schema(this.model).one();
}
}
function useStoreActions(query) {
return {
save(records, triggerQueryAction = true) {
this.data = Object.assign({}, this.data, records);
if (triggerQueryAction && query) {
query.newQuery(this.$id).save(Object.values(records));
}
},
insert(records, triggerQueryAction = true) {
this.data = Object.assign({}, this.data, records);
if (triggerQueryAction && query) {
query.newQuery(this.$id).insert(Object.values(records));
}
},
update(records, triggerQueryAction = true) {
this.data = Object.assign({}, this.data, records);
if (triggerQueryAction && query) {
query.newQuery(this.$id).update(Object.values(records));
}
},
fresh(records, triggerQueryAction = true) {
this.data = records;
if (triggerQueryAction && query) {
query.newQuery(this.$id).fresh(Object.values(records));
}
},
destroy(ids, triggerQueryAction = true) {
if (triggerQueryAction && query) {
query.newQuery(this.$id).newQuery(this.$id).destroy(ids);
} else {
ids.forEach((id) => delete this.data[id]);
if (this.data.__ob__) {
this.data.__ob__.dep.notify();
}
}
},
/**
* Commit `delete` change to the store.
*/
delete(ids, triggerQueryAction = true) {
if (triggerQueryAction && query) {
query.whereId(ids).delete();
} else {
ids.forEach((id) => delete this.data[id]);
if (this.data.__ob__) {
this.data.__ob__.dep.notify();
}
}
},
flush(_records, triggerQueryAction = true) {
this.data = {};
if (triggerQueryAction && query) {
query.newQuery(this.$id).flush();
}
}
};
}
function useDataStore(id, options, customOptions, query) {
if (config.pinia.storeType === "optionStore") {
return defineStore(id, {
state: () => ({ data: {} }),
actions: useStoreActions(query),
...options
});
}
return defineStore(id, () => ({
data: ref({}),
...useStoreActions(query),
...options
}), customOptions);
}
class BelongsToMany extends Relation {
/**
* The pivot model.
*/
pivot;
/**
* The foreign key of the parent model.
*/
foreignPivotKey;
/**
* The associated key of the relation.
*/
relatedPivotKey;
/**
* The key name of the parent model.
*/
parentKey;
/**
* The key name of the related model.
*/
relatedKey;
/**
* The key name of the pivot data.
*/
pivotKey = "pivot";
/**
* Create a new belongs to instance.
*/
constructor(parent, related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
super(parent, related);
this.pivot = pivot;
this.foreignPivotKey = foreignPivotKey;
this.relatedPivotKey = relatedPivotKey;
this.parentKey = parentKey;
this.relatedKey = relatedKey;
}
/**
* Get all related models for the relationship.
*/
getRelateds() {
return [this.related, this.pivot];
}
/**
* Define the normalizr schema for the relationship.
*/
define(schema) {
return schema.many(this.related, this.parent);
}
/**
* Attach the parent type and id to the given relation.
*/
attach(record, child) {
const pivot = child[this.pivotKey] ?? {};
pivot[this.foreignPivotKey] = record[this.parentKey];
pivot[this.relatedPivotKey] = child[this.relatedKey];
child[`pivot_${this.relatedPivotKey}_${this.pivot.$entity()}`] = pivot;
}
/**
* Convert given value to the appropriate value for the attribute.
*/
make(elements) {
return elements ? elements.map((element) => this.related.$newInstance(element)) : [];
}
/**
* Match the eagerly loaded results to their parents.
*/
match(relation, models, query) {
const relatedModels = query.get(false);
const pivotModels = query.newQuery(this.pivot.$modelEntity()).whereIn(this.relatedPivotKey, this.getKeys(relatedModels, this.relatedKey)).whereIn(this.foreignPivotKey, this.getKeys(models, this.parentKey)).groupBy(this.foreignPivotKey, this.relatedPivotKey).get();
models.forEach((parentModel) => {
const relationResults = [];
relatedModels.forEach((relatedModel) => {
const pivot = pivotModels[`[${parentModel[this.parentKey]},${relatedModel[this.relatedKey]}]`]?.[0] ?? null;
if (!pivot) {
return;
}
const relatedModelCopy = relatedModel.$newInstance(relatedModel.$toJson(), { operation: void 0 });
delete relatedModelCopy[`pivot_${this.relatedPivotKey}_${this.pivot.$entity()}`];
relatedModelCopy.$setRelation(this.pivotKey, pivot, true);
relationResults.push(relatedModelCopy);
});
parentModel.$setRelation(relation, relationResults);
parentModel.$setRelation(this.pivotKey, void 0);
});
}
/**
* Set the constraints for the related relation.
*/
addEagerConstraints(_query, _collection) {
}
/**
* Specify the custom pivot accessor to use for the relationship.
*/
as(accessor) {
this.pivotKey = accessor;
return this;
}
}
class Query {
/**
* The database instance.
*/
database;
/**
* The model object.
*/
model;
/**
* The where constraints for the query.
*/
wheres = [];
/**
* The orderings for the query.
*/
orders = [];
/**
* The orderings for the query.
*/
groups = [];
/**
* The maximum number of records to return.
*/
take = null;
/**
* The number of records to skip.
*/
skip = 0;
/**
* Fields that should be visible.
*/
visible = ["*"];
/**
* Fields that should be hidden.
*/
hidden = [];
/**
* The cache object.
*/
cache;
/**
* The relationships that should be eager loaded.
*/
eagerLoad = {};
/**
* The pinia store.
*/
pinia;
fromCache = false;
cacheConfig = {};
getNewHydrated = false;
/**
* Hydrated models. They are stored to prevent rerendering of child components.
*/
hydratedDataCache;
/**
* Create a new query instance.
*/
constructor(database, model, cache, hydratedData, pinia) {
this.database = database;
this.model = model;
this.pinia = pinia;
this.cache = cache;
this.hydratedDataCache = hydratedData;
this.getNewHydrated = false;
}
/**
* Create a new query instance for the given model.
*/
newQuery(model) {
this.getNewHydrated = true;
return new Query(this.database, this.database.getModel(model), this.cache, this.hydratedDataCache, this.pinia);
}
/**
* Create a new query instance with constraints for the given model.
*/
newQueryWithConstraints(model) {
const newQuery = new Query(this.database, this.database.getModel(model), this.cache, this.hydratedDataCache, this.pinia);
newQuery.eagerLoad = { ...this.eagerLoad };
newQuery.wheres = [...this.wheres];
newQuery.orders = [...this.orders];
newQuery.take = this.take;
newQuery.skip = this.skip;
newQuery.fromCache = this.fromCache;
newQuery.cacheConfig = this.cacheConfig;
return newQuery;
}
/**
* Create a new query instance from the given relation.
*/
newQueryForRelation(relation) {
return new Query(this.database, relation.getRelated(), this.cache, /* @__PURE__ */ new Map(), this.pinia);
}
/**
* Create a new interpreter instance.
*/
newInterpreter() {
return new Interpreter(this.model);
}
/**
* Commit a store action and get the data
*/
commit(name, payload) {
const store = useDataStore(this.model.$storeName(), this.model.$piniaOptions(), this.model.$piniaExtend(), this)(this.pinia);
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(store, import.meta.hot));
}
if (name && name !== "all" && name !== "get" && typeof store[name] === "function") {
store[name](payload, false);
}
if (this.cache && ["get", "all", "insert", "flush", "delete", "update", "destroy"].includes(name)) {
this.cache.clear();
}
return store.$state.data;
}
/**
* Make meta field visible
*/
withMeta() {
return this.makeVisible(["_meta"]);
}
/**
* Make hidden fields visible
*/
makeVisible(fields) {
this.visible = fields;
this.getNewHydrated = true;
return this;
}
/**
* Make visible fields hidden
*/
makeHidden(fields) {
this.hidden = fields;
this.getNewHydrated = true;
return this;
}
// where(field: T, value?: WhereSecondaryClosure<M[T]> | M[T]): this;
// where<T extends WherePrimaryClosure<M> | keyof M>(field: T, value?: WhereSecondaryClosure<M[T]> | M[T]): this;
/**
* Add a basic where clause to the query.
*/
where(field, value) {
this.wheres.push({ field, value, boolean: "and" });
return this;
}
/**
* Add a "where in" clause to the query.
*/
whereIn(field, values) {
if (values instanceof Set) {
values = Array.from(values);
}
return this.where(field, values);
}
/**
* Add a "where not in" clause to the query.
*/
whereNotIn(field, values) {
if (values instanceof Set) {
values = Array.from(values);
}
return this.where((query) => !values.includes(query[field]));
}
/**
* Add a "where not in" clause to the query.
*/
orWhereIn(field, values) {
if (values instanceof Set) {
values = Array.from(values);
}
return this.orWhere(field, values);
}
/**
* Add a "where not in" clause to the query.
*/
orWhereNotIn(field, values) {
if (values instanceof Set) {
values = Array.from(values);
}
return this.orWhere((query) => !values.includes(query[field]));
}
/**
* Add a where clause on the primary key to the query.
*/
whereId(ids) {
return this.where(this.model.$getKeyName(), ids);
}
/**
* Add an "or where" clause to the query.
*/
orWhere(field, value) {
this.wheres.push({ field, value, boolean: "or" });
return this;
}
/**
* Add a "whereNULL" clause to the query.
*/
whereNull(field) {
return this.where(field, null);
}
/**
* Add a "whereNotNULL" clause to the query.
*/
whereNotNull(field) {
return this.where((query) => query[field] != null);
}
/**
* Add a "where has" clause to the query.
*/
whereHas(relation, callback = () => {
}, operator, count) {
return this.where(this.getFieldWhereForRelations(relation, callback, operator, count));
}
/**
* Add an "or where has" clause to the query.
*/
orWhereHas(relation, callback = () => {
}, operator, count) {
return this.orWhere(this.getFieldWhereForRelations(relation, callback, operator, count));
}
/**
* Add a "has" clause to the query.
*/
has(relation, operator, count) {
return this.where(this.getFieldWhereForRelations(relation, () => {
}, operator, count));
}
/**
* Add an "or has" clause to the query.
*/
orHas(relation, operator, count) {
return this.orWhere(this.getFieldWhereForRelations(relation, () => {
}, operator, count));
}
/**
* Add a "doesn't have" clause to the query.
*/
doesntHave(relation) {
return this.where(this.getFieldWhereForRelations(relation, () => {
}, "=", 0));
}
/**
* Add a "doesn't have" clause to the query.
*/
orDoesntHave(relation) {
return this.orWhere(this.getFieldWhereForRelations(relation, () => {
}, "=", 0));
}
/**
* Add a "where doesn't have" clause to the query.
*/
whereDoesntHave(relation, callback = () => {
}) {
return this.where(this.getFieldWhereForRelations(relation, callback, "=", 0));
}
/**
* Add an "or where doesn't have" clause to the query.
*/
orWhereDoesntHave(relation, callback = () => {
}) {
return this.orWhere(this.getFieldWhereForRelations(relation, callback, "=", 0));
}
/**
* Add a "group by" clause to the query.
*/
groupBy(...fields) {
fields.forEach((field) => {
this.groups.push({ field });
});
return this;
}
/**
* Add an "order by" clause to the query.
*/
orderBy(field, direction = "asc") {
this.orders.push({ field, direction });
return this;
}
/**
* Set the "limit" value of the query.
*/
limit(value) {
this.take = value;
return this;
}
/**
* Set the "offset" value of the query.
*/
offset(value) {
this.skip = value;
return this;
}
/**
* Set the relationships that should be eager loaded.
*/
with(name, callback = () => {
}) {
this.getNewHydrated = true;
this.eagerLoad[name] = callback;
return this;
}
/**
* Set to eager load all top-level relationships. Constraint is set for all relationships.
*/
withAll(callback = () => {
}) {
let fields = this.model.$fields();
const typeModels = Object.values(this.model.$types());
typeModels.forEach((typeModel) => {
fields = { ...fields, ...typeModel.fields() };
});
for (const name in fields) {
fields[name] instanceof Relation && this.with(name, callback);
}
return this;
}
/**
* Set to eager load all relationships recursively.
*/
withAllRecursive(depth = 3) {
return this.withAll((query) => {
depth > 0 && query.withAllRecursive(depth - 1);
});
}
/**
* Define to use the cache for a query
*/
useCache(key, params) {
this.fromCache = true;
this.cacheConfig = {
key,
params
};
return this;
}
/**
* Get where closure for relations
*/
getFieldWhereForRelations(relation, callback = () => {
}, operator, count) {
const modelIdsByRelation = this.newQuery(this.model.$entity()).with(relation, callback).get(false).filter((model) => {
const modelRelation = model[relation];
return compareWithOperator(
isArray(modelRelation) ? modelRelation.length : modelRelation === null ? 0 : 1,
typeof operator === "number" ? operator : count ?? 1,
typeof operator === "number" || count === void 0 ? ">=" : operator
);
}).map((model) => model.$getIndexId());
return (model) => modelIdsByRelation.includes(model.$getIndexId());
}
/**
* Get all models by id from the store. The difference with the `get` is that this
* method will not process any query chain.
*/
storeFind(ids = []) {
const data = this.commit("all");
const collection = [];
const deduplicatedIds = new Set(ids);
if (deduplicatedIds.size > 0) {
deduplicatedIds.forEach((id) => {
if (data[id]) {
collection.push(this.hydrate(data[id], { visible: this.visible, hidden: this.hidden, operation: "get" }));
}
});
} else {
Object.values(data).forEach((value) => collection.push(this.hydrate(value, { visible: this.visible, hidden: this.hidden, operation: "get" })));
}
return collection;
}
/**
* Get all models from the store. The difference with the `get` is that this
* method will not process any query chain. It'll always retrieve all models.
*/
all() {
return this.storeFind();
}
get(triggerHook = true) {
if (!this.fromCache || !this.cache) {
return this.internalGet(triggerHook);
}
const key = this.cacheConfig.key ? this.cacheConfig.key + JSON.stringify(this.cacheConfig.params) : generateKey(this.model.$entity(), {
where: this.wheres,
groups: this.groups,
orders: this.orders,
eagerLoads: this.eagerLoad,
skip: this.skip,
take: this.take,
hidden: this.hidden,
visible: this.visible
});
const result = this.cache.get(key);
if (result) {
return result;
}
const queryResult = this.internalGet(triggerHook);
this.cache.set(key, queryResult);
return queryResult;
}
internalGet(triggerHook) {
if (this.model.$entity() !== this.model.$baseEntity() || this.model.$namespace() !== this.model.$baseNamespace()) {
const typeKeyValue = this.model.$fields()[this.model.$typeKey()].make() ?? this.model.$entity();
this.where(this.model.$typeKey(), typeKeyValue);
}
let models = this.select();
if (this.orders.length === 0) {
models = this.filterLimit(models);
}
if (!isEmpty(models)) {
this.eagerLoadRelations(models);
}
if (this.orders.length > 0) {
models = this.filterOrder(models);
models = this.filterLimit(models);
}
if (triggerHook) {
models.forEach((model) => model.$self().retrieved(model));
}
if (this.groups.length > 0) {
return this.filterGroup(models);
}
return models;
}
/**
* Execute the query and get the first result.
*/
first() {
return this.limit(1).get()[0] ?? null;
}
find(ids) {
return this.whereId(ids)[isArray(ids) ? "get" : "first"]();
}
/**
* Retrieve models by processing all filters set to the query chain.
*/
select() {
let ids = [];
const originalWheres = this.wheres;
const whereIdsIndex = this.wheres.findIndex((where) => where.field === this.model.$getKeyName());
if (whereIdsIndex > -1) {
const whereIds = this.wheres[whereIdsIndex].value;
ids = ((isFunction(whereIds) ? [] : isArray(whereIds) ? whereIds : [whereIds]) || []).map(String) || [];
if (ids.length > 0) {
this.wheres = [...this.wheres.slice(0, whereIdsIndex), ...this.wheres.slice(whereIdsIndex + 1)];
}
}
let models = this.storeFind(ids);
models = this.filterWhere(models);
this.wheres = originalWheres;
return models;
}
/**
* Filter the given collection by the registered where clause.
*/
filterWhere(models) {
if (isEmpty(this.wheres)) {
return models;
}
const comparator = this.getWhereComparator();
return models.filter((model) => comparator(model));
}
/**
* Get comparator for the where clause.
*/
getWhereComparator() {
const { and, or } = groupBy(this.wheres, (where) => where.boolean);
return (model) => {
const results = [];
and && results.push(and.every((w) => this.whereComparator(model, w)));
or && results.push(or.some((w) => this.whereComparator(model, w)));
return results.includes(true);
};
}
/**
* The function to compare where clause to the given model.
*/
whereComparator(model, where) {
if (isFunction(where.field)) {
return where.field(model);
}
if (isArray(where.value)) {
return where.value.includes(model[where.field]);
}
if (isFunction(where.value)) {
return where.value(model[where.field]);
}
return model[where.field] === where.value;
}
/**
* Filter the given collection by the registered order conditions.
*/
filterOrder(models) {
const fields = this.orders.map((order) => order.field);
const directions = this.orders.map((order) => order.direction);
return orderBy(models, fields, directions);
}
/**
* Filter the given collection by the registered group conditions.
*/
filterGroup(models) {
const grouped = {};
const fields = this.groups.map((group) => group.field);
models.forEach((model) => {
const key = fields.length === 1 ? model[fields[0]] : `[${fields.map((field) => model[field]).toString()}]`;
grouped[key] = (grouped[key] || []).concat(model);
});
return grouped;
}
/**
* Filter the given collection by the registered limit and offset values.
*/
filterLimit(models) {
return this.take !== null ? models.slice(this.skip, this.skip + this.take) : models.slice(this.skip);
}
/**
* Eager load relations on the model.
*/
load(models) {
this.eagerLoadRelations(models);
}
/**
* Eager load the relationships for the models.
*/
eagerLoadRelations(models) {
for (const name in this.eagerLoad) {
this.eagerLoadRelation(models, name, this.eagerLoad[name]);
}
}
/**
* Eagerly load the relationship on a set of models.
*/
eagerLoadRelation(models, name, constraints) {
const relation = this.getRelation(name);
const query = this.newQueryForRelation(relation);
relation.addEagerConstraints(query, models);
constraints(query);
relation.match(name, models, query);
}
/**
* Get the relation instance for the given relation name.
*/
getRelation(name) {
return this.model.$getRelation(name);
}
revive(schema) {
return isArray(schema) ? this.reviveMany(schema) : this.reviveOne(schema);
}
/**
* Revive single model from the given schema.
*/
reviveOne(schema) {
this.getNewHydrated = false;
const id = this.model.$getIndexId(schema);
const item = this.commit("get")[id] ?? null;
if (!item) {
return null;
}
const model = this.hydrate(item, { visible: this.visible, hidden: this.hidden, operation: "get" });
this.reviveRelations(model, schema);
return model;
}
/**
* Revive multiple models from the given schema.
*/
reviveMany(schema) {
return schema.reduce((collection, item) => {
const model = this.reviveOne(item);
model && collection.push(model);
return collection;
}, []);
}
/**
* Revive relations for the given schema and entity.
*/
reviveRelations(model, schema) {
const fields = this.model.$fields();
for (const key in schema) {
const attr = fields[key];
if (!(attr instanceof Relation)) {
continue;
}
const relatedSchema = schema[key];
if (!relatedSchema) {
return;
}
if (attr instanceof MorphTo) {
const relatedType = model[attr.getType()];
model[key] = this.newQuery(relatedType).reviveOne(relatedSchema);
continue;
}
model[key] = isArray(relatedSchema) ? this.newQueryForRelation(attr).reviveMany(relatedSchema) : this.newQueryForRelation(attr).reviveOne(relatedSchema);
}
}
/**
* Create and persist model with default values.
*/
new(persist = true) {
let model = this.hydrate({}, { operation: persist ? "set" : "get" });
const isCreating = model.$self().creating(model);
const isSaving = model.$self().saving(model);
if (isCreating === false || isSaving === false) {
return null;
}
if (model.$isDirty()) {
model = this.hydrate(model.$getAttributes(), { operation: persist ? "set" : "get" });
}
if (persist) {
this.hydratedDataCache.set(this.model.$entity() + model.$getKey(void 0, true), this.hydrate(model.$getAttributes(), { operation: "get" }));
model.$self().created(model);
model.$self().saved(model);
this.commit("insert", this.compile(model));
}
return model;
}
save(records) {
let processedData = this.newInterpreter().process(records);
const modelTypes = this.model.$types();
const isChildEntity = this.model.$baseEntity() !== this.model.$entity() || this.model.$baseNamespace() !== this.model.$namespace();
if (Object.values(modelTypes).length > 0 || isChildEntity) {
const modelTypesKeys = Object.keys(modelTypes);
const recordsByTypes = {};
records = isArray(records) ? records : [records];
records.forEach((record) => {
const recordType = modelTypesKeys.includes(`${record[this.model.$typeKey()]}`) || isChildEntity ? record[this.model.$typeKey()] ?? this.model.$fields()[this.model.$typeKey()].defaultValue : modelTypesKeys[0];
if (!recordsByTypes[recordType]) {
recordsByTypes[recordType] = [];
}
recordsByTypes[recordType].push(record);
});
for (const entry in recordsByTypes) {
const typeModel = modelTypes[entry];
if (typeModel.modelEntity() === this.model.$modelEntity()) {
processedData = this.newInterpreter().process(recordsByTypes[entry]);
} else {
this.newQueryWithConstraints(typeModel.modelEntity()).save(recordsByTypes[entry]);
}
}
}
const [data, entities] = processedData;
for (const entity in entities) {
const query = this.newQuery(entity);
const elements = entities[entity];
query.saveElements(elements);
}
return this.revive(data);
}
/**
* Save the given elements to the store.
*/
saveElements(elements) {
const newData = {};
const currentData = this.commit("all");
const afterSavingHooks = [];
for (const id in elements) {
const record = elements[id];
const existing = currentData[id];
let model = existing ? Object.assign(this.hydrate(existing, { operation: "set", action: "update" }), record) : this.hydrate(record, { operation: "set", action: "save" });
const isSaving = model.$self().saving(model, record);
const isUpdatingOrCreating = existing ? model.$self().updating(model, record) : model.$self().creating(model, record);
if (isSaving === false || isUpdatingOrCreating === false) {
continue;
}
if (model.$isDirty()) {
model = this.hydrate(model.$getAttributes(), { operation: "set", action: "update" });
}
afterSavingHooks.push(() => model.$self().saved(model, record));
afterSavingHooks.push(() => existing ? model.$self().updated(model, record) : model.$self().created(model, record));
newData[id] = model.$getAttributes();
if (Object.values(model.$types()).length > 0 && !newData[id][model.$typeKey()]) {
newData[id][model.$typeKey()] = record[model.$typeKey()];
}
}
if (Object.keys(newData).length > 0) {
this.commit("save", newData);
afterSavingHooks.forEach((hook) => hook());
}
}
insert(records) {
const models = this.hydrate(records, { operation: "set", action: "insert" });
this.commit("insert", this.compile(models));
return models;
}
fresh(records) {
this.hydratedDataCache.clear();
const models = this.hydrate(records, { action: "update" });
this.commit("fresh", this.compile(models));
return models;
}
/**
* Update the reocrd matching the query chain.
*/
update(record) {
const models = this.get(false);
if (isEmpty(models)) {
return [];
}
const newModels = models.map((model) => {
const oldModelUpdate = Object.assign(this.hydrate(model.$getAttributes(), { action: "update", operation: "set" }), record);
if (model.$self().updating(oldModelUpdate, record) === false) {
return model;
}
const newModel = oldModelUpdate.$isDirty() ? this.hydrate({ ...model.$getAttributes(), ...record }, { action: "update", operation: "set" }) : oldModelUpdate;
newModel.$self().updated(newModel, record);
return newModel;
});
this.commit("update", this.compile(newModels));
return newModels;
}
destroy(ids) {
return isArray(ids) ? this.destroyMany(ids) : this.destroyOne(ids);
}
destroyOne(id) {
const model = this.find(id);
if (!model) {
return null;
}
const [afterHooks, removeIds] = this.dispatchDeleteHooks(model);
if (!removeIds.includes(model.$getIndexId())) {
this.commit("destroy", [model.$getIndexId()]);
afterHooks.forEach((hook) => hook());
}
return model;
}
destroyMany(ids) {
const models = this.find(ids);
if (isEmpty(models)) {
return [];
}
const [afterHooks, removeIds] = this.dispatchDeleteHooks(models);
const checkedIds = this.getIndexIdsFromCollection(models).filter((id) => !removeIds.includes(id));
this.commit("destroy", checkedIds);
afterHooks.forEach((hook) => hook());
return models;
}
/**
* Delete records resolved by the query chain.
*/
delete() {
const models = this.get(false);
if (isEmpty(models)) {
return [];
}
const [afterHooks, removeIds] = this.dispatchDeleteHooks(models);
const ids = this.getIndexIdsFromCollection(models).filter((id) => !removeIds.includes(id));
this.commit("delete", ids);
afterHooks.forEach((hook) => hook());
return models;
}
/**
* Delete all records in the store.
*/
flush() {
this.commit("flush");
this.hydratedDataCache.clear();
return this.get(false);
}
checkAndDeleteRelations(model) {
const fields = model.$fields();
for (const name in fields) {
const relation = fields[name];
if (fields[name] instanceof Relation && relation.onDeleteMode && model[name]) {
const models = isArray(model[name]) ? model[name] : [model[name]];
const relationIds = models.map((relation2) => {
return relation2.$getKey(void 0, true);
});
const record = {};
if (relation instanceof BelongsToMany) {
this.newQuery(relation.pivot.$entity()).where(relation.foreignPivotKey, model[model.$getLocalKey()]).delete();
continue;
}
switch (relation.onDeleteMode) {
case "cascade": {
this.newQueryForRelation(relation).destroy(relationIds);
break;
}
case "set null": {
if (relation.foreignKey) {
record[relation.foreignKey] = null;
}
if (relation.morphId) {
record[relation.morphId] = null;
record[relation.morphType] = null;
}
this.newQueryForRelation(relation).whereId(relationIds).update(record);
break;
}
}
}
}
}
dispatchDeleteHooks(models) {
const afterHooks = [];
const notDeletableIds = [];
models = isArray(models) ? models : [models];
this.withAll().load(models);
models.forEach((currentModel) => {
const isDeleting = currentModel.$self().deleting(currentModel);
if (isDeleting === false) {
notDeletableIds.push(currentModel.$getIndexId());
} else {
this.hydratedDataCache.delete("set" + this.model.$entity() + currentModel.$getIndexId());
this.hydratedDataCache.delete("get" + this.model.$entity() + currentModel.$getIndexId());
afterHooks.push(() => currentModel.$self().deleted(currentModel));
this.checkAndDeleteRelations(currentModel);
}
});
return [afterHooks, notDeletableIds];
}
/**
* Get an array of index ids from the given collection.
*/
getIndexIdsFromCollection(models) {
return models.map((model) => model.$getIndexId());
}
hydrate(records, options) {
return isArray(records) ? records.map((record) => this.hydrate(record, options)) : this.getHydratedModel(records, { relations: false, ...options || {} });
}
/**
* Convert given models into an indexed object that is ready to be saved to
* the store.
*/
compile(models) {
const collection = isArray(models) ? models : [models];
return collection.reduce((records, model) => {
records[model.$getIndexId()] = model.$getAttributes();
return records;
}, {});
}
/**
* Save already existing models and return them if they exist to prevent
* an update event trigger in vue if the object is used.
*/
getHydratedModel(record, options) {
const id = this.model.$entity() + this.model.$getKey(record, true);
const operationId = options?.operation + id;
let savedHydratedModel = this.hydratedDataCache.get(operationId);
if (options?.action === "update") {
this.hydratedDataCache.delete("get" + id);
savedHydratedModel = void 0;
}
if (!this.getNewHydrated && savedHydratedModel) {
return savedHydratedModel;
}
const modelByType = this.model.$types()[record[this.model.$typeKey()]];
const getNewInsance = (newOptions) => (modelByType ? modelByType.newRawInstance() : this.model).$newInstance(record, { relations: false, ...options || {}, ...newOptions });
const hydratedModel = getNewInsance();
if (isEmpty(this.eagerLoad) && options?.operation !== "set") {
this.hydratedDataCache.set(operationId, hydratedModel);
}
return hydratedModel;
}
}
class WeakCache {
// @ts-expect-error dont know
[Symbol.toStringTag];
#map = /* @__PURE__ */ new Map();
has(key) {
return !!(this.#map.has(key) && this.#map.get(key)?.deref());
}
get(key) {
const weakRef = this.#map.get(key);
if (!weakRef) {
return void 0;
}
const value = weakRef.deref();
if (value) {
return value;
}
this.#map.delete(key);
return void 0;
}
set(key, value) {
this.#map.set(key, new WeakRef(value));
return this;
}
get size() {
return this.#map.size;
}
clear() {
this.#map.clear();
}
delete(key) {
this.#map.delete(key);
return false;
}
forEach(cb) {
for (const [key, value] of this) {
cb(value, key, this);
}
}
*[Symbol.iterator]() {
for (const [key, weakRef] of this.#map) {
const ref = weakRef.deref();
if (!ref) {
this.#map.delete(key);
continue;
}
yield [key, ref];
}
}
*entries() {
for (const [key, value] of this) {
yield [key, value];
}
}
*keys() {
for (const [key] of this) {
yield key;
}
}
*values() {
for (const [, value] of this) {
yield value;
}
}
}
const cache$1 = new WeakCache();
const cache = /* @__PURE__ */ new Map();
const CONFIG_DEFAULTS = {
model: {
namespace: "",
withMeta: false,
hidden: ["_meta"],
visible: ["*"]
},
cache: {
shared: true,
provider: WeakCache
},
pinia: {
storeType: "optionStore"
}
};
const config = { ...CONFIG_DEFAULTS };
class Repository {
/**
* A special flag to indicate if this is the repository class or not. It's
* used when retrieving repository instance from `store.$repo()` method to
* determine whether the passed in class is either a repository or a model.
*/
static _isRepository = true;
/**
* The database instance.
*/
database;
/**
* The model instance.
*/
model;
/**
* The pinia instance
*/
pinia;
/**
* The cache instance
*/
queryCache;
/**
* Hydrated models. They are stored to prevent rerendering of child components.
*/
hydratedDataCache;
/**
* The model object to be used for the custom repository.
*/
use;
/**
* The model object to be used for the custom repository.
*/
static useModel;
/**
* Global config
*/
config;
/**
* Create a new Repository instance.
*/
constructor(database, pinia) {
this.config = config;
this.database = database;
this.pinia = pinia;
this.hydratedDataCache = cache;
return new Proxy(this, {
get(repository, field) {
if (typeof field === "symbol") {
return;
}
if (field in repository) {
return repository[field];
}
if (field === "use" || field === "model" || field === "queryCache") {
return;
}
return function(...args) {
return repository.query()[field](...args);
};
}
});
}
/**
* Set the model
*/
static setModel(model) {
this.useModel = model;
return this;
}
/**
* Set the global config
*/
setConfig(config) {
this.config = config;
}
/**
* Initialize the repository by setting the model instance.
*/
initialize(model) {
if (this.config.cache && this.config.cache !== true) {
this.queryCache = this.config.cache.shared ? cache$1 : new this.config.cache.provider();
}
if (model) {
this.model = model.newRawInstance();
return this;
}
if (this.use || this.$self().useModel) {
this.use = this.use ?? this.$self().useModel;
this.model = this.use.newRawInstance();
return this;
}
return this;
}
/**
* Get the constructor for this model.
*/
$self() {
return this.constructor;
}
/**
* Get the model instance. If the model is not registered to the repository,
* it will throw an error. It happens when users use a custom repository
* without setting `use` property.
*/
getModel() {
assert(!!this.model, [
"The model is not registered. Please define the model to be used at",
"`use` property of the repository class."
]);
return this.model;
}
/**
* Returns the pinia store used with this model
*/
piniaStore() {
return useDataStore(this.model.$storeName(), this.model.$piniaOptions(), this.model.$piniaExtend(), this.query())(this.pinia);
}
repo(modelOrRepository) {
return useRepo(modelOrRepository);
}
/**
* Create a new Query instance.
*/
query() {
return new Query(this.database, this.getModel(), this.queryCache, this.hydratedDataCache, this.pinia);
}
/**
* Create a new Query instance.
*/
cache() {
return this.queryCache;
}
/**
* Add a basic where clause to the query.
*/
where(field, value) {
return this.query().where(field, value);
}
/**
* Add an "or where" clause to the query.
*/
orWhere(field, value) {
return this.query().orW