UNPKG

pinia-orm

Version:

The Pinia plugin to enable Object-Relational Mapping access to the Pinia Store.

1,744 lines (1,728 loc) 101 kB
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