From f5f62ae4cf3f6c9a8fb7d90e296246f61d007662 Mon Sep 17 00:00:00 2001 From: Kia King Ishii Date: Wed, 1 Apr 2020 01:07:12 +0900 Subject: [PATCH 01/56] feat: add repository class (#585) (#591) --- src/database/Database.ts | 18 +- src/database/HackedDatabase.ts | 28 +++ src/index.cjs.ts | 3 + src/index.ts | 4 + src/model/Model.ts | 2 +- src/plugins/use.ts | 3 + src/query/Query.ts | 4 +- src/repository/Repository.ts | 166 ++++++++++++++++ src/store/install.ts | 84 +++++++- src/types/index.ts | 1 + src/types/vuex.ts | 27 ++- test/feature/repositories/Crud_Create.spec.ts | 31 +++ test/feature/repositories/Crud_Delete.spec.ts | 74 +++++++ test/feature/repositories/Crud_Insert.spec.ts | 31 +++ .../repositories/Crud_InsertOrUpdate.spec.ts | 39 ++++ test/feature/repositories/Crud_New.spec.ts | 29 +++ .../repositories/Crud_Retrieve.spec.ts | 185 ++++++++++++++++++ test/feature/repositories/Crud_Update.spec.ts | 35 ++++ test/feature/repositories/Repository.spec.ts | 100 ++++++++++ test/unit/database/Database.spec.ts | 72 +++---- test/unit/query/Query.spec.ts | 8 +- test/unit/support/Utils.spec.ts | 97 +++------ tslint.json | 5 +- 23 files changed, 917 insertions(+), 129 deletions(-) create mode 100644 src/database/HackedDatabase.ts create mode 100644 src/repository/Repository.ts create mode 100644 src/types/index.ts create mode 100644 test/feature/repositories/Crud_Create.spec.ts create mode 100644 test/feature/repositories/Crud_Delete.spec.ts create mode 100644 test/feature/repositories/Crud_Insert.spec.ts create mode 100644 test/feature/repositories/Crud_InsertOrUpdate.spec.ts create mode 100644 test/feature/repositories/Crud_New.spec.ts create mode 100644 test/feature/repositories/Crud_Retrieve.spec.ts create mode 100644 test/feature/repositories/Crud_Update.spec.ts create mode 100644 test/feature/repositories/Repository.spec.ts diff --git a/src/database/Database.ts b/src/database/Database.ts index 881c524c..e07c5b5d 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -45,6 +45,11 @@ export default class Database { */ entities: Entity[] = [] + /** + * The hacked models registered to the store. + */ + hackedModels: Models = {} + /** * The normalizr schema. */ @@ -63,8 +68,6 @@ export default class Database { this.store = store this.namespace = namespace - this.connect() - this.registerModules() this.createSchema() @@ -80,10 +83,12 @@ export default class Database { const entity: Entity = { name: model.entity, base: model.baseEntity || model.entity, - model: this.createBindingModel(model), + model, module } + this.hackedModels[model.entity] = this.createBindingModel(model) + this.entities.push(entity) if (this.isStarted) { @@ -356,13 +361,6 @@ export default class Database { this.schemas[entity.name] = Schema.create(entity.model) } - /** - * Inject database to the store instance. - */ - private connect (): void { - this.store.$db = () => this - } - /** * Warn user if the given model is a type of an inherited model that is being * defined without overwriting `Model.types()` because the user will not be diff --git a/src/database/HackedDatabase.ts b/src/database/HackedDatabase.ts new file mode 100644 index 00000000..c5cb2c5f --- /dev/null +++ b/src/database/HackedDatabase.ts @@ -0,0 +1,28 @@ +import Database from './Database' +import Model from '../model/Model' + +export default class HackedDatabase { + /** + * The database instance to look up models. + */ + database: Database + + /** + * Create a new hacked database. + */ + constructor (database: Database) { + this.database = database + } + + /** + * Get the hacked model of the given name from the entities list. + */ + model (model: M): M + model (model: string): typeof Model + model (model: typeof Model | string): typeof Model | string { + /* istanbul ignore next */ + const name = typeof model === 'string' ? model : model.entity + + return this.database.hackedModels[name] + } +} diff --git a/src/index.cjs.ts b/src/index.cjs.ts index d069bf2f..32496d6f 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -26,6 +26,7 @@ import MorphOne from './attributes/relations/MorphOne' import MorphMany from './attributes/relations/MorphMany' import MorphToMany from './attributes/relations/MorphToMany' import MorphedByMany from './attributes/relations/MorphedByMany' +import Repository from './repository/Repository' import Getters from './modules/Getters' import Actions from './modules/Actions' import RootGetters from './modules/RootGetters' @@ -64,6 +65,7 @@ export interface VuexORM { MorphMany: typeof MorphMany MorphToMany: typeof MorphToMany MorphedByMany: typeof MorphedByMany + Repository: typeof Repository Getters: GettersContract Actions: ActionsContract RootGetters: RootGettersContract @@ -96,6 +98,7 @@ export default { MorphMany, MorphToMany, MorphedByMany, + Repository, Getters, Actions, RootGetters, diff --git a/src/index.ts b/src/index.ts index d2ebacd2..ead67a3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import MorphOne from './attributes/relations/MorphOne' import MorphMany from './attributes/relations/MorphMany' import MorphToMany from './attributes/relations/MorphToMany' import MorphedByMany from './attributes/relations/MorphedByMany' +import Repository from './repository/Repository' import Getters from './modules/Getters' import Actions from './modules/Actions' import RootGetters from './modules/RootGetters' @@ -73,6 +74,7 @@ export interface VuexORM { MorphMany: typeof MorphMany MorphToMany: typeof MorphToMany MorphedByMany: typeof MorphedByMany + Repository: typeof Repository Getters: GettersContract Actions: ActionsContract RootGetters: RootGettersContract @@ -108,6 +110,7 @@ export { MorphMany, MorphToMany, MorphedByMany, + Repository, Getters, Actions, RootGetters, @@ -151,6 +154,7 @@ export default { MorphMany, MorphToMany, MorphedByMany, + Repository, Getters, Actions, RootGetters, diff --git a/src/model/Model.ts b/src/model/Model.ts index 65de2bb5..812184c9 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -284,7 +284,7 @@ export default class Model { * Get the database instance from store. */ static database (): Database { - return this.store().$db() + return this.store().$database } /** diff --git a/src/plugins/use.ts b/src/plugins/use.ts index b42c2b6e..2f077cdb 100644 --- a/src/plugins/use.ts +++ b/src/plugins/use.ts @@ -1,3 +1,4 @@ +import Repository from '../repository/Repository' import Model from '../model/Model' import Attribute from '../attributes/Attribute' import Type from '../attributes/types/Type' @@ -32,6 +33,7 @@ import Query from '../query/Query' import Database from '../database/Database' export interface PluginComponents { + Repository: typeof Repository Model: typeof Model Attribute: typeof Attribute Type: typeof Type @@ -73,6 +75,7 @@ export type Use = (plugin: Plugin, options?: Options) => void export default function (plugin: Plugin, options: Options = {}): void { const components: PluginComponents = { + Repository, Model, Attribute, Type, diff --git a/src/query/Query.ts b/src/query/Query.ts index d531a3e3..a5428261 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -139,7 +139,7 @@ export default class Query { */ constructor (store: Store, entity: string) { this.store = store - this.database = store.$db() + this.database = store.$database this.model = this.getModel(entity) this.baseModel = this.getBaseModel(entity) @@ -157,7 +157,7 @@ export default class Query { * Delete all records from the store. */ static deleteAll (store: Store): void { - const database = store.$db() + const database = store.$database const models = database.models() for (const entity in models) { diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts new file mode 100644 index 00000000..b8f19284 --- /dev/null +++ b/src/repository/Repository.ts @@ -0,0 +1,166 @@ +import { Store } from 'vuex' +import { ConstructorOf as BaseConstructorOf } from '../types' +import Database from '../database/Database' +import { Record, Item, Collection, Collections } from '../data' +import Model from '../model/Model' +import * as Payloads from '../modules/payloads/Actions' +import Query from '../query/Query' +import Predicate from '../query/contracts/Predicate' + +interface ConstructorOf extends BaseConstructorOf { + entity: string +} + +export default 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: boolean = true + + /** + * The store instance for the repository. + */ + store: Store + + /** + * The model for the repository. + */ + model!: ConstructorOf + + /** + * Create a new repository instance. + */ + constructor (store: Store, model?: ConstructorOf) { + this.store = store + + if (model) { + this.model = model + } + } + + /** + * Get the database instance from the store instance. + */ + database (): Database { + return this.store.$database + } + + /** + * Create a namespaced method name for Vuex Module from the given + * method name. + */ + namespace (method: string): string { + return `${this.database().namespace}/${this.model.entity}/${method}` + } + + /** + * Call Vuex Getetrs. + */ + getters (method: string): any { + return this.store.getters[this.namespace(method)] + } + + /** + * Dispatch Vuex Action. + */ + dispatch (method: string, payload?: any): Promise { + return this.store.dispatch(this.namespace(method), payload) + } + + /** + * Create a new model instance. + */ + make (record?: Record): M { + return new this.model(record) + } + + /** + * Get all records. + */ + all (): Collection { + return this.getters('all')() + } + + /** + * Find a record. + */ + find (id: string | number | (number | string)[]): Item { + return this.getters('find')(id) + } + + /** + * Get the record of the given array of ids. + */ + findIn (idList: (number | string | (number | string)[])[]): Collection { + return this.getters('findIn')(idList) + } + + /** + * Get query instance. + */ + query (): Query { + return this.getters('query')() + } + + /** + * Check wether the associated database contains data. + */ + exists (): boolean { + return this.query().exists() + } + + /** + * Create new data with all fields filled by default values. + */ + new (): Promise { + return this.dispatch('new') + } + + /** + * Save given data to the store by replacing all existing records in the + * store. If you want to save data without replacing existing records, + * use the `insert` method instead. + */ + create (payload: Payloads.Create): Promise { + return this.dispatch('create', payload) + } + + /** + * Insert records. + */ + insert (payload: Payloads.Insert): Promise { + return this.dispatch('insert', payload) + } + + /** + * Update records. + */ + update (payload: Payloads.Update): Promise { + return this.dispatch('update', payload) + } + + /** + * Insert or update records. + */ + insertOrUpdate (payload: Payloads.InsertOrUpdate): Promise { + return this.dispatch('insertOrUpdate', payload) + } + + /** + * Delete records that matches the given condition. + */ + delete (id: string | number | (number | string)[]): Promise> + delete (condition: Predicate): Promise> + delete (payload: any): any { + return this.dispatch('delete', payload) + } + + /** + * Delete all records from the store. + */ + deleteAll (): Promise> { + return this.dispatch('deleteAll') + } +} diff --git a/src/store/install.ts b/src/store/install.ts index b450483b..4589fa4b 100644 --- a/src/store/install.ts +++ b/src/store/install.ts @@ -1,19 +1,91 @@ -import * as Vuex from 'vuex' +import { Store, Plugin } from 'vuex' import Container from '../container/Container' import Database from '../database/Database' +import HackedDatabase from '../database/HackedDatabase' +import Repository from '../repository/Repository' -export type Install = (database: Database, options?: Options) => Vuex.Plugin +export type Install = (database: Database, options?: Options) => Plugin export interface Options { namespace?: string } -export default (database: Database, options: Options = {}): Vuex.Plugin => { +/** + * Install Vuex ORM database to the store. + */ +export default function install (database: Database, options: Options = {}): Plugin { const namespace = options.namespace || 'entities' - return (store: Vuex.Store): void => { - database.start(store, namespace) + return (store) => { mixin(store, database, namespace) } +} + +/** + * Mixin Vuex ORM feature to the store. + */ +function mixin (store: Store, database: Database, namespace: string): void { + registerDatabase(store, database) + mixinRepoFunction(store) + mixinDbFunction(store) + registerToContainer(store) + startDatabase(store, namespace) +} + +/** + * Register the database to the store. + */ +function registerDatabase (store: Store, database: Database): void { + store.$database = database +} + +/** + * Mixin repo function to the store. + */ +function mixinRepoFunction (store: Store): void { + store.$repo = function (modelOrRepository: any): any { + const repository = modelOrRepository._isRepository + ? new modelOrRepository(this) + : new Repository(this, modelOrRepository) - Container.register(store) + if (!repository.model) { + throw new Error( + '[Vuex ORM] The repository was instantiated without a model being ' + + 'set. It happens when you forgot to register the model to the ' + + 'custom repository. Please check if you have correctly set `model` ' + + 'property at the repository class.' + ) + } + + return repository } } + +/** + * Mixin db function to the store. + */ +function mixinDbFunction (store: Store): void { + store.$db = function (): HackedDatabase { + /* istanbul ignore next */ + if (process.env.NODE_ENV !== 'production') { + console.warn( + '[Vuex ORM] `store.$db()` method is deprecated. Please use ' + + '`store.$repo(Model)` method instead.' + ) + } + + return new HackedDatabase(this.$database) + } +} + +/** + * Register the store to the container. + */ +function registerToContainer (store: Store): void { + Container.register(store) +} + +/** + * Start the database. + */ +function startDatabase (store: Store, namespace: string): void { + store.$database.start(store, namespace) +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..c5ddf026 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export type ConstructorOf = { new (...args: any[]): C } diff --git a/src/types/vuex.ts b/src/types/vuex.ts index 1fdea2f3..0dcb78e2 100644 --- a/src/types/vuex.ts +++ b/src/types/vuex.ts @@ -1,7 +1,32 @@ +import { ConstructorOf } from '../types' import Database from '../database/Database' +import HackedDatabase from '../database/HackedDatabase' +import Model from '../model/Model' +import Repository from '../repository/Repository' declare module 'vuex' { interface Store { - $db (): Database + /** + * The database instance registered to the store. + */ + $database: Database + + /** + * Get a new repository instance for the given model. + */ + $repo > (repository: ConstructorOf): R + $repo (model: ConstructorOf): Repository + + /** + * Get the database attached to the store. It's the old syntax and should + * avoid using it. + * + * It will return a "Hacked" database that capable of finding a copied + * model object. It's an old way of interacting with the database. + * + * TODO: Update the version. + * @deprecated Since v0.XX.X + */ + $db (): HackedDatabase } } diff --git a/test/feature/repositories/Crud_Create.spec.ts b/test/feature/repositories/Crud_Create.spec.ts new file mode 100644 index 00000000..a177a8ac --- /dev/null +++ b/test/feature/repositories/Crud_Create.spec.ts @@ -0,0 +1,31 @@ +import { createStore, createState } from 'test/support/Helpers' +import { Model } from '@/index' + +describe('Feature - Repositories - CRUD Create', () => { + it('can create given record to the store', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + const store = createStore([User]) + + await store.$repo(User).create({ + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Crud_Delete.spec.ts b/test/feature/repositories/Crud_Delete.spec.ts new file mode 100644 index 00000000..b6d7b320 --- /dev/null +++ b/test/feature/repositories/Crud_Delete.spec.ts @@ -0,0 +1,74 @@ +import { createStore, createState } from 'test/support/Helpers' +import { Model } from '@/index' + +describe('Feature - Repositories - CRUD Delete', () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null) + } + } + + id!: any + } + + it('can delete a record by specifying the id', async () => { + const store = createStore([User]) + + const user = store.$repo(User) + + await user.insert({ + data: [{ id: 1 }, { id: 2 }] + }) + + await user.delete(1) + + const expected = createState({ + users: { + 2: { $id: '2', id: 2 } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can delete records by specifying a closure', async () => { + const store = createStore([User]) + + const user = store.$repo(User) + + await user.insert({ + data: [{ id: 1 }, { id: 2 }] + }) + + await user.delete(user => user.id === 2) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1 } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can delete all records in the entity', async () => { + const store = createStore([User]) + + const user = store.$repo(User) + + await user.insert({ + data: [{ id: 1 }, { id: 2 }] + }) + + await user.deleteAll() + + const expected = createState({ + users: {} + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Crud_Insert.spec.ts b/test/feature/repositories/Crud_Insert.spec.ts new file mode 100644 index 00000000..08c5bddf --- /dev/null +++ b/test/feature/repositories/Crud_Insert.spec.ts @@ -0,0 +1,31 @@ +import { createStore, createState } from 'test/support/Helpers' +import { Model } from '@/index' + +describe('Feature - Repositories - CRUD Insert', () => { + it('can insert given record to the store', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + const store = createStore([User]) + + await store.$repo(User).insert({ + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Crud_InsertOrUpdate.spec.ts b/test/feature/repositories/Crud_InsertOrUpdate.spec.ts new file mode 100644 index 00000000..91df28fa --- /dev/null +++ b/test/feature/repositories/Crud_InsertOrUpdate.spec.ts @@ -0,0 +1,39 @@ +import { createStore, createState } from 'test/support/Helpers' +import { Model } from '@/index' + +describe('Feature - Repositories - CRUD Insert or Update', () => { + it('can insert or update given record to the store', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + const store = createStore([User]) + + await store.$repo(User).insert({ + data: { id: 1, name: 'John Doe' } + }) + + await store.$repo(User).insertOrUpdate({ + data: [ + { id: 1, name: 'Jane Doe' }, + { id: 2, name: 'Johnny Doe' } + ] + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'Jane Doe' }, + 2: { $id: '2', id: 2, name: 'Johnny Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Crud_New.spec.ts b/test/feature/repositories/Crud_New.spec.ts new file mode 100644 index 00000000..b55c0002 --- /dev/null +++ b/test/feature/repositories/Crud_New.spec.ts @@ -0,0 +1,29 @@ +import { createStore, createState } from 'test/support/Helpers' +import { Model } from '@/index' + +describe('Feature - Repositories - CRUD New', () => { + it('can insert a new record to the store', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.uid(), + name: this.string('John Doe') + } + } + } + + const store = createStore([User]) + + await store.$repo(User).new() + + const expected = createState({ + users: { + '$uid1': { $id: '$uid1', id: '$uid1', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Crud_Retrieve.spec.ts b/test/feature/repositories/Crud_Retrieve.spec.ts new file mode 100644 index 00000000..e24f39b0 --- /dev/null +++ b/test/feature/repositories/Crud_Retrieve.spec.ts @@ -0,0 +1,185 @@ +import { createStore } from 'test/support/Helpers' +import { Model, Query } from '@/index' + +describe('Feature - Repositories - CRUD Retrieve', () => { + it('can get a new query instance', () => { + class User extends Model { + static entity = 'users' + } + + const store = createStore([User]) + + const query = store.$repo(User).query() + + expect(query).toBeInstanceOf(Query) + }) + + it('can get all records', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null) + } + } + + id!: number + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + await userRepo.insert({ + data: [{ id: 1 }, { id: 2 }] + }) + + const users = userRepo.all() + + expect(users.length).toBe(2) + expect(users[0].id).toBe(1) + expect(users[1].id).toBe(2) + }) + + it('can find a single record', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null) + } + } + + id!: number + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + await userRepo.insert({ + data: [{ id: 1 }, { id: 2 }] + }) + + const user = userRepo.find(2) as User + + expect(user.id).toBe(2) + }) + + it('can find a single record with composite primary key', async () => { + class User extends Model { + static entity = 'users' + + static primaryKey = ['idA', 'idB'] + + static fields () { + return { + idA: this.attr(null), + idB: this.attr(null) + } + } + + idA!: number + idB!: number + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + await userRepo.insert({ + data: [{ idA: 1, idB: 2 }, { idA: 3, idB: 4 }] + }) + + const user = userRepo.find([1, 2]) as User + + expect(user.idA).toBe(1) + expect(user.idB).toBe(2) + }) + + it('can find records by ids', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null) + } + } + + id!: number + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + await userRepo.insert({ + data: [{ id: 1 }, { id: 2 }, { id: 3 }] + }) + + const users = userRepo.findIn([1, 3]) + + expect(users[0].id).toBe(1) + expect(users[1].id).toBe(3) + }) + + it('can find records by ids with composite primary key', async () => { + class User extends Model { + static entity = 'users' + + static primaryKey = ['idA', 'idB'] + + static fields () { + return { + idA: this.attr(null), + idB: this.attr(null) + } + } + + idA!: number + idB!: number + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + await userRepo.insert({ + data: [{ idA: 1, idB: 2 }, { idA: 3, idB: 4 }, { idA: 5, idB: 6 }] + }) + + const users = userRepo.findIn([[1, 2], [5, 6]]) + + expect(users[0].idA).toBe(1) + expect(users[0].idB).toBe(2) + expect(users[1].idA).toBe(5) + expect(users[1].idB).toBe(6) + }) + + it('can check whether record exists', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null) + } + } + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + expect(userRepo.exists()).toBe(false) + + await userRepo.insert({ + data: { id: 1 } + }) + + expect(userRepo.exists()).toBe(true) + }) +}) diff --git a/test/feature/repositories/Crud_Update.spec.ts b/test/feature/repositories/Crud_Update.spec.ts new file mode 100644 index 00000000..6acad1e2 --- /dev/null +++ b/test/feature/repositories/Crud_Update.spec.ts @@ -0,0 +1,35 @@ +import { createStore, createState } from 'test/support/Helpers' +import { Model } from '@/index' + +describe('Feature - Repositories - CRUD Update', () => { + it('can update record by including primary key in the data', async () => { + class User extends Model { + static entity = 'users' + + static fields () { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + const store = createStore([User]) + + const user = store.$repo(User) + + await user.insert({ + data: { id: 1, name: 'John Doe' } + }) + + await user.update({ id: 1, name: 'Jane Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Repository.spec.ts b/test/feature/repositories/Repository.spec.ts new file mode 100644 index 00000000..9bef239c --- /dev/null +++ b/test/feature/repositories/Repository.spec.ts @@ -0,0 +1,100 @@ +import { createStore } from 'test/support/Helpers' +import Repository from '@/repository/Repository' +import Model from '@/model/Model' + +describe('Feature - Repository', () => { + it('can retrieve a new repository instance from the store', () => { + class User extends Model { + static entity = 'users' + } + + const store = createStore([User]) + + const user = store.$repo(User) + + expect(user).toBeInstanceOf(Repository) + }) + + it('can instantiate a new model instance without a record', () => { + class User extends Model { + static entity = 'users' + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + expect(userRepo.make()).toBeInstanceOf(User) + }) + + it('can instantiate a new model instance with a record', () => { + class User extends Model { + static entity = 'users' + + // @Attribute('') + name!: string + + // @Attribute('') + email!: string + + static fields () { + return { + id: this.attr(''), + name: this.attr('') + } + } + } + + const store = createStore([User]) + + const userRepo = store.$repo(User) + + const user = userRepo.make({ name: 'John Doe' }) + + expect(user).toBeInstanceOf(User) + expect(user.name).toBe('John Doe') + }) + + it('injects store instance to the instantiated model.', () => { + class User extends Model { + static entity = 'users' + } + + const store = createStore([User]) + + const user = store.$repo(User).make() + + expect(user.$store()).toBe(store) + }) + + it('can retrieve a custom repository', () => { + class User extends Model { + static entity = 'users' + } + + class UserRepository extends Repository { + model = User + foo = 'bar' + } + + const store = createStore([User]) + + const userRepo = store.$repo(UserRepository) + + expect(userRepo.foo).toBe('bar') + }) + + it('throws if the custom repository has no assigned model', () => { + class User extends Model { + static entity = 'users' + } + + class UserRepository extends Repository { + foo = 'bar' + } + + const store = createStore([User]) + + expect(() => store.$repo(UserRepository)).toThrow() + }) +}) diff --git a/test/unit/database/Database.spec.ts b/test/unit/database/Database.spec.ts index ea67335d..793d604e 100644 --- a/test/unit/database/Database.spec.ts +++ b/test/unit/database/Database.spec.ts @@ -3,30 +3,6 @@ import Database from '@/database/Database' import Model from '@/model/Model' describe('Unit – Database', () => { - it('registers itself to the store instance', () => { - const database = new Database() - - const store = createStoreFromDatabase(database) - - expect(store.$db()).toBe(database) - }) - - it('binds the store instance to the registered models.', () => { - class User extends Model { static entity = 'users' } - - const database1 = new Database() - const database2 = new Database() - - database1.register(User) - database2.register(User) - - const store1 = createStoreFromDatabase(database1) - const store2 = createStoreFromDatabase(database2) - - expect(store1.$db().model('users').database()).toBe(database1) - expect(store2.$db().model('users').database()).toBe(database2) - }) - it('can fetch all models', () => { class User extends Model { static entity = 'users' } class Post extends Model { static entity = 'posts' } @@ -38,8 +14,8 @@ describe('Unit – Database', () => { const models = database.models() - expect(models.users.prototype).toBeInstanceOf(User) - expect(models.posts.prototype).toBeInstanceOf(Post) + expect(models.users).toBe(User) + expect(models.posts).toBe(Post) }) it('can fetch a model by string', () => { @@ -53,7 +29,7 @@ describe('Unit – Database', () => { const model = database.model('users') - expect(model.prototype).toBeInstanceOf(User) + expect(model).toBe(User) }) it('can fetch a model by type', () => { @@ -67,7 +43,7 @@ describe('Unit – Database', () => { const model = database.model(User) - expect(model.prototype).toBeInstanceOf(User) + expect(model).toBe(User) }) it('throws when a model is not found', () => { @@ -96,9 +72,9 @@ describe('Unit – Database', () => { const models = database.baseModels() - expect(models.users.prototype).toBeInstanceOf(User) - expect(models.guests.prototype).toBeInstanceOf(User) - expect(models.admins.prototype).toBeInstanceOf(User) + expect(models.users).toBe(User) + expect(models.guests).toBe(User) + expect(models.admins).toBe(User) }) it('can fetch a base model by string', () => { @@ -115,7 +91,7 @@ describe('Unit – Database', () => { const model = database.baseModel('guests') - expect(model.prototype).toBeInstanceOf(User) + expect(model).toBe(User) }) it('can fetch a base model by type', () => { @@ -132,7 +108,7 @@ describe('Unit – Database', () => { const model = database.baseModel(Guest) - expect(model.prototype).toBeInstanceOf(User) + expect(model).toBe(User) }) it('throws when a base model is not found', () => { @@ -196,4 +172,34 @@ describe('Unit – Database', () => { expect(() => database.module('posts')).toThrowError('[Vuex ORM]') }) + + it('binds the store instance to the registered models.', () => { + jest.spyOn(global.console, 'warn').mockImplementation() + + class User extends Model { static entity = 'users' } + + const database1 = new Database() + const database2 = new Database() + + database1.register(User) + database2.register(User) + + const store1 = createStoreFromDatabase(database1) + const store2 = createStoreFromDatabase(database2) + + expect(store1.$db().model('users').database()).toBe(database1) + expect(store2.$db().model('users').database()).toBe(database2) + }) + + it('warns when using the deprecated `$db` method', () => { + const warn = jest.spyOn(global.console, 'warn').mockImplementation() + + const database = new Database() + + const store = createStoreFromDatabase(database) + + store.$db() + + expect(warn).toBeCalled() + }) }) diff --git a/test/unit/query/Query.spec.ts b/test/unit/query/Query.spec.ts index f8e83a7b..591ba930 100644 --- a/test/unit/query/Query.spec.ts +++ b/test/unit/query/Query.spec.ts @@ -16,8 +16,8 @@ describe('Unit – Query', () => { const models = (new Query(store, 'users')).getModels() - expect(models.users.prototype).toBeInstanceOf(User) - expect(models.posts.prototype).toBeInstanceOf(Post) + expect(models.users).toBe(User) + expect(models.posts).toBe(Post) }) it('can retrieve a specific model from the database instance', () => { @@ -25,7 +25,7 @@ describe('Unit – Query', () => { const model = (new Query(store, 'users')).getModel('posts') - expect(model.prototype).toBeInstanceOf(Post) + expect(model).toBe(Post) }) it('retrieves the model corresponds to the entity of Query if the name is not specified', () => { @@ -33,6 +33,6 @@ describe('Unit – Query', () => { const model = (new Query(store, 'users')).getModel() - expect(model.prototype).toBeInstanceOf(User) + expect(model).toBe(User) }) }) diff --git a/test/unit/support/Utils.spec.ts b/test/unit/support/Utils.spec.ts index 85ca822b..1faa6906 100644 --- a/test/unit/support/Utils.spec.ts +++ b/test/unit/support/Utils.spec.ts @@ -124,82 +124,37 @@ describe('Unit - Utils', () => { }) describe('#cloneDeep', () => { - it('should clone arrays', async () => { - const a = [{ a: 0 }, { b: 1 }] - - expect(Utils.cloneDeep(a)).toEqual(a) - - const b = [1, 2, 3] - - expect(b).toEqual(b) - - const c = [{ a: 0 }, { b: 1 }] - const d = Utils.cloneDeep(c) - - expect(d).toEqual(c) - expect(d[0]).toEqual(c[0]) - - const e = [0, 'a', {}, [{}]] - - expect(Utils.cloneDeep(e)).toEqual(e) - }) - - it('should deeply clone an array', async () => { - const a = [[{ a: 'b' }], [{ a: 'b' }]] - const b = Utils.cloneDeep(a) - - expect(b).not.toBe(a) - expect(b[0]).not.toBe(a[0]) - expect(b[1]).not.toBe(a[1]) - expect(b).toEqual(a) - }) - - it('should deeply clone arrays by reference', async () => { - const a: any = { a: 'b' } - const b = [a] - const c = Utils.cloneDeep(b) - - a.c = 'd' - - expect(c).not.toEqual(b) - }) - - it('should clone objects', () => { - const a = { a: 1, b: 2, c: 3 } - - expect(Utils.cloneDeep(a)).toEqual(a) - expect(Utils.cloneDeep(a)).not.toBe(a) - }) - - it('should deeply clone objects', async () => { - const a = { - a: { a: 1, b: 2, c: 3 }, - b: { a: 1, b: 2, c: 3 }, - c: { a: 1, b: 2, c: 3 } + it('can create a deep clone of an object', () => { + const data = { + id: 1, + nested: [ + { + id: 1, + deep: [ + { + id: 1, + deeper: { + id: 2 + } + }, + { + id: 2, + deeper: { + id: 3 + } + } + ] + } + ] } - expect(Utils.cloneDeep(a)).toEqual(a) - expect(Utils.cloneDeep(a)).not.toBe(a) - expect(Utils.cloneDeep(a).a).toEqual(a.a) - expect(Utils.cloneDeep(a).a).not.toBe(a.a) - expect(Utils.cloneDeep(a).b).toEqual(a.b) - expect(Utils.cloneDeep(a).b).not.toBe(a.b) - expect(Utils.cloneDeep(a).c).toEqual(a.c) - expect(Utils.cloneDeep(a).c).not.toBe(a.c) - }) + const clone = Utils.cloneDeep(data) - it('should deeply clone objects by reference', async () => { - const a: Record = { a: 'b' } - const b = Utils.cloneDeep(a) + expect(clone).toStrictEqual(data) - b.c = 'd' - - expect(b).not.toEqual(a) - }) + delete data.nested[0].deep[0].deeper.id - it('should return primitives', async () => { - expect(Utils.cloneDeep('foo')).toEqual('foo') - expect(Utils.cloneDeep(0)).toEqual(0) + expect(clone).not.toStrictEqual(data) }) }) }) diff --git a/tslint.json b/tslint.json index 88877985..7b861221 100644 --- a/tslint.json +++ b/tslint.json @@ -1,3 +1,6 @@ { - "extends": "tslint-config-standard" + "extends": "tslint-config-standard", + "rules": { + "deprecation": false + } } From 4b31ac3b7c3f36c6e878613c5bd5e6ccc7b5fbf0 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:29:36 +0100 Subject: [PATCH 02/56] feat(model): enable passing data to persistent methods (#554) close #554 --- src/model/Model.ts | 56 +-- src/modules/Actions.ts | 28 +- src/modules/options/Actions.ts | 13 + src/modules/payloads/Actions.ts | 21 +- src/modules/support/PayloadBuilder.ts | 44 +++ test/feature/aggregates/Aggregates.spec.ts | 54 ++- test/feature/attributes/Boolean.spec.ts | 66 ++-- test/feature/attributes/Number.spec.ts | 58 ++- test/feature/attributes/String.spec.ts | 48 ++- test/feature/attributes/Uid.spec.ts | 13 +- test/feature/basics/Create.spec.ts | 16 +- test/feature/basics/Delete.spec.ts | 66 ++-- .../basics/Delete_Composite_Key.spec.ts | 17 +- test/feature/basics/Exists.spec.ts | 12 +- test/feature/basics/Insert.spec.ts | 6 +- test/feature/basics/PersistOptions.spec.ts | 78 ++--- test/feature/basics/Retrieve.spec.ts | 20 +- test/feature/basics/Update.spec.ts | 16 +- test/feature/hooks/Global.spec.ts | 9 +- test/feature/hooks/Global_Create.spec.ts | 16 +- test/feature/hooks/Global_Delete.spec.ts | 12 +- test/feature/hooks/Global_Update.spec.ts | 24 +- test/feature/hooks/Local_Binding.spec.ts | 4 +- test/feature/hooks/Local_Create.spec.ts | 22 +- test/feature/hooks/Local_Delete.spec.ts | 24 +- test/feature/hooks/Local_Insert.spec.ts | 12 +- test/feature/hooks/Local_Update.spec.ts | 22 +- test/feature/inheritance/Inheritance.spec.ts | 16 +- .../inheritance/Inheritance_CRUD.spec.ts | 119 +++---- .../Inheritance_Discriminator.spec.ts | 36 +- .../inheritance/Inheritance_Relations.spec.ts | 94 ++--- test/feature/models/All.spec.ts | 20 +- test/feature/models/Create.spec.ts | 56 ++- test/feature/models/Insert.spec.ts | 64 ++-- test/feature/models/InsertOrUpdate.spec.ts | 36 +- test/feature/models/Query.spec.ts | 20 +- test/feature/models/Update.spec.ts | 13 +- .../relations/BelongsToMany_Retrieve.spec.ts | 101 ++---- test/feature/relations/HasMany.spec.ts | 159 ++++----- test/feature/relations/HasManyBy.spec.ts | 46 +-- test/feature/relations/HasManyThrough.spec.ts | 170 ++++----- test/feature/relations/HasOne.spec.ts | 58 ++- .../relations/MorphMany_Persist.spec.ts | 132 +++---- .../relations/MorphMany_Retrieve.spec.ts | 78 ++--- .../relations/MorphOne_Persist.spec.ts | 96 +++-- .../relations/MorphOne_Retrieve.spec.ts | 82 ++--- test/feature/relations/MorphTo.spec.ts | 136 ++++++-- .../relations/MorphToMany_Persist.spec.ts | 121 ++++--- .../relations/MorphToMany_Retrieve.spec.ts | 126 +++---- .../relations/MorphedByMany_Persist.spec.ts | 50 ++- .../relations/MorphedByMany_Retrieve.spec.ts | 92 ++--- .../relations/Persist_NestedMorph.spec.ts | 24 +- .../relations/Retrieve_Constraint.spec.ts | 12 +- .../relations/Retrieve_DeepLoad.spec.ts | 22 +- test/feature/relations/Retrieve_Has.spec.ts | 171 +++++---- .../relations/Retrieve_SelfRelation.spec.ts | 9 +- .../relations/Retrieve_StringDefine.spec.ts | 8 +- test/feature/relations/Retrieve_With.spec.ts | 330 ++++++++---------- .../Retrieve_With_NestedConstraints.spec.ts | 16 +- 59 files changed, 1459 insertions(+), 1831 deletions(-) create mode 100644 src/modules/options/Actions.ts create mode 100644 src/modules/support/PayloadBuilder.ts diff --git a/src/model/Model.ts b/src/model/Model.ts index 812184c9..d9998532 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -15,11 +15,13 @@ import Mutators from '../attributes/contracts/Mutators' import Predicate from '../query/contracts/Predicate' import Query from '../query/Query' import * as Payloads from '../modules/payloads/Actions' +import * as PersistOptions from '../modules/options/Actions' import Fields from './contracts/Fields' import FieldCache from './contracts/FieldCache' import ModelState from './contracts/State' import InheritanceTypes from './contracts/InheritanceTypes' import { toAttributes, toJson } from './Serialize' +import PayloadBuilder from '../modules/support/PayloadBuilder' export default class Model { /** @@ -383,29 +385,29 @@ export default class Model { * store. If you want to save data without replacing existing records, * use the `insert` method instead. */ - static create (this: T, payload: Payloads.Create): Promise { - return this.dispatch('create', payload) + static create (this: T, payload: Payloads.Create, options?: PersistOptions.Create): Promise { + return this.dispatch('create', PayloadBuilder.normalize(payload, options)) } /** * Insert records. */ - static insert (this: T, payload: Payloads.Insert): Promise { - return this.dispatch('insert', payload) + static insert (this: T, payload: Payloads.Insert, options?: PersistOptions.Insert): Promise { + return this.dispatch('insert', PayloadBuilder.normalize(payload, options)) } /** * Update records. */ - static update (this: T, payload: Payloads.Update): Promise { - return this.dispatch('update', payload) + static update (this: T, payload: Payloads.Update, options?: PersistOptions.Update): Promise { + return this.dispatch('update', PayloadBuilder.normalize(payload, options)) } /** * Insert or update records. */ - static insertOrUpdate (this: T, payload: Payloads.InsertOrUpdate): Promise { - return this.dispatch('insertOrUpdate', payload) + static insertOrUpdate (this: T, payload: Payloads.InsertOrUpdate, options?: PersistOptions.InsertOrUpdate): Promise { + return this.dispatch('insertOrUpdate', PayloadBuilder.normalize(payload, options)) } /** @@ -686,7 +688,7 @@ export default class Model { } /** - * Call Vuex Getetrs. + * Call Vuex Getters. */ $getters (method: string): any { return this.$self().getters(method) @@ -730,44 +732,48 @@ export default class Model { /** * Create records. */ - async $create (payload: Payloads.Create): Promise { - return this.$dispatch('create', payload) + async $create (payload: Payloads.Create, options?: PersistOptions.Create): Promise { + return this.$dispatch('create', PayloadBuilder.normalize(payload, options)) } /** - * Create records. + * Insert records. */ - async $insert (payload: Payloads.Insert): Promise { - return this.$dispatch('insert', payload) + async $insert (payload: Payloads.Insert, options?: PersistOptions.Insert): Promise { + return this.$dispatch('insert', PayloadBuilder.normalize(payload, options)) } /** * Update records. */ - async $update (payload: Payloads.Update): Promise { + async $update (payload: Payloads.Update, options?: PersistOptions.Update): Promise { if (Utils.isArray(payload)) { - return this.$dispatch('update', payload) + return this.$dispatch('update', PayloadBuilder.normalize(payload, options)) } - if (payload.where !== undefined) { - return this.$dispatch('update', payload) + if (payload.where !== undefined || (options && options.where !== undefined)) { + return this.$dispatch('update', PayloadBuilder.normalize(payload, options)) } if (this.$self().getIndexIdFromRecord(payload) === null) { - return this.$dispatch('update', { - where: this.$self().getIdFromRecord(this), - data: payload - }) + /* istanbul ignore else */ + if (!options) { + options = {} + } + + options.where = this.$self().getIdFromRecord(this) + + return this.$dispatch('update', PayloadBuilder.normalize(payload, options)) } - return this.$dispatch('update', payload) + return this.$dispatch('update', PayloadBuilder.normalize(payload, options)) } /** * Insert or update records. */ - async $insertOrUpdate (payload: Payloads.InsertOrUpdate): Promise { - return this.$dispatch('insertOrUpdate', payload) + async $insertOrUpdate (payload: Payloads.InsertOrUpdate, options?: PersistOptions.InsertOrUpdate): Promise { + return this.$dispatch('insertOrUpdate', PayloadBuilder.normalize(payload, options)) } /** diff --git a/src/modules/Actions.ts b/src/modules/Actions.ts index c43f083d..a44e0947 100644 --- a/src/modules/Actions.ts +++ b/src/modules/Actions.ts @@ -1,4 +1,3 @@ -import { isArray } from '../support/Utils' import Item from '../data/Item' import Collection from '../data/Collection' import Collections from '../data/Collections' @@ -6,6 +5,7 @@ import Model from '../model/Model' import ActionsContract from './contracts/Actions' import ActionContext from './contracts/ActionContext' import * as Payloads from './payloads/Actions' +import PayloadBuilder from './support/PayloadBuilder' /** * Create new data with all fields filled by default values. @@ -25,8 +25,9 @@ async function newRecord (context: ActionContext): Promise { async function create (context: ActionContext, payload: Payloads.Create): Promise { const state = context.state const entity = state.$name + const data = PayloadBuilder.createPersistPayload(payload) - return context.dispatch(`${state.$connection}/create`, { ...payload, entity }, { root: true }) + return context.dispatch(`${state.$connection}/create`, { entity, ...data }, { root: true }) } /** @@ -37,8 +38,9 @@ async function create (context: ActionContext, payload: Payloads.Create): Promis async function insert (context: ActionContext, payload: Payloads.Insert): Promise { const state = context.state const entity = state.$name + const data = PayloadBuilder.createPersistPayload(payload) - return context.dispatch(`${state.$connection}/insert`, { ...payload, entity }, { root: true }) + return context.dispatch(`${state.$connection}/insert`, { entity, ...data }, { root: true }) } /** @@ -47,22 +49,9 @@ async function insert (context: ActionContext, payload: Payloads.Insert): Promis async function update (context: ActionContext, payload: Payloads.Update): Promise { const state = context.state const entity = state.$name + const data = PayloadBuilder.createPersistPayload(payload) - // If the payload is an array, then the payload should be an array of - // data so let's pass the whole payload as data. - if (isArray(payload)) { - return context.dispatch(`${state.$connection}/update`, { entity, data: payload }, { root: true }) - } - - // If the payload doesn't have `data` property, we'll assume that - // the user has passed the object as the payload so let's define - // the whole payload as a data. - if (payload.data === undefined) { - return context.dispatch(`${state.$connection}/update`, { entity, data: payload }, { root: true }) - } - - // Else destructure the payload and let root action handle it. - return context.dispatch(`${state.$connection}/update`, { entity, ...payload }, { root: true }) + return context.dispatch(`${state.$connection}/update`, { entity, ...data }, { root: true }) } /** @@ -73,8 +62,9 @@ async function update (context: ActionContext, payload: Payloads.Update): Promis async function insertOrUpdate (context: ActionContext, payload: Payloads.InsertOrUpdate): Promise { const state = context.state const entity = state.$name + const data = PayloadBuilder.createPersistPayload(payload) - return context.dispatch(`${state.$connection}/insertOrUpdate`, { entity, ...payload }, { root: true }) + return context.dispatch(`${state.$connection}/insertOrUpdate`, { entity, ...data }, { root: true }) } /** diff --git a/src/modules/options/Actions.ts b/src/modules/options/Actions.ts new file mode 100644 index 00000000..4629e0db --- /dev/null +++ b/src/modules/options/Actions.ts @@ -0,0 +1,13 @@ +import { Condition } from '../payloads/Actions' +import PrimaryKey from '../../query/options/PrimaryKey' +import PersistOptions from '../../query/options/PersistOptions' + +export type Create = PersistOptions + +export type Insert = PersistOptions + +export interface Update extends PersistOptions { + where?: PrimaryKey | Condition | null +} + +export type InsertOrUpdate = PersistOptions diff --git a/src/modules/payloads/Actions.ts b/src/modules/payloads/Actions.ts index 11be4e4f..5cc0e997 100644 --- a/src/modules/payloads/Actions.ts +++ b/src/modules/payloads/Actions.ts @@ -4,12 +4,18 @@ import PersistOptions from '../../query/options/PersistOptions' export type Condition = (record: Record) => boolean -export interface Create extends PersistOptions { - data: Record | Record[] +export type Create = CreateObject | Record[] + +export interface CreateObject extends PersistOptions { + data?: Record | Record[] + [key: string]: any } -export interface Insert extends PersistOptions { - data: Record | Record[] +export type Insert = InsertObject | Record[] + +export interface InsertObject extends PersistOptions { + data?: Record | Record[] + [key: string]: any } export type Update = UpdateObject | Record[] @@ -20,8 +26,11 @@ export interface UpdateObject extends PersistOptions { [key: string]: any } -export interface InsertOrUpdate extends PersistOptions { - data: Record | Record[] +export type InsertOrUpdate = InsertOrUpdateObject | Record[] + +export interface InsertOrUpdateObject extends PersistOptions { + data?: Record | Record[] + [key: string]: any } export type DeleteById = string | number | (number | string)[] diff --git a/src/modules/support/PayloadBuilder.ts b/src/modules/support/PayloadBuilder.ts new file mode 100644 index 00000000..985f83da --- /dev/null +++ b/src/modules/support/PayloadBuilder.ts @@ -0,0 +1,44 @@ +import { Create, Insert, Update, InsertOrUpdate } from '../payloads/Actions' +import OptionsBuilder from './OptionsBuilder' + +export type PersistPayload = Create | Insert | Update | InsertOrUpdate + +export default class PayloadBuilder { + /** + * Create downstream payload from input data to determine the style of data + * being provided for downward compatibility. + */ + static createPersistPayload (payload: PersistPayload): PersistPayload { + // If the payload is an array, then the payload should be an array of + // data therefore the whole payload should be declared as `data`. + if (Array.isArray(payload)) { + return { data: payload } + } + + // If the payload doesn't have a `data` property, it can be assumed that + // the user has passed an object as the payload therefore the entire + // payload should be declared as `data` + if (payload.data === undefined) { + return { data: payload } + } + + // It can safely be assumed the user is providing payload with `data` intact. + return payload + } + + /** + * Normalize persist payload by converting the new style of persisting data + * through method arguments, while maintaining existing style (data key), + * to pass on to the module API. + */ + static normalize (payload?: any, options?: any): PersistPayload { + const persistPayload = this.createPersistPayload(payload) + const persistOptions = OptionsBuilder.createPersistOptions(payload) + + return { + ...persistPayload, + ...persistOptions, + ...options + } + } +} diff --git a/test/feature/aggregates/Aggregates.spec.ts b/test/feature/aggregates/Aggregates.spec.ts index f5d6a11a..1c6f77f9 100644 --- a/test/feature/aggregates/Aggregates.spec.ts +++ b/test/feature/aggregates/Aggregates.spec.ts @@ -24,27 +24,23 @@ describe('Feature – Aggregates', () => { }) it('can get count of the data', async () => { - await User.create({ - data: [ - { id: 1, role: 'admin' }, - { id: 2, role: 'user' }, - { id: 3, role: 'admin' } - ] - }) + await User.create([ + { id: 1, role: 'admin' }, + { id: 2, role: 'user' }, + { id: 3, role: 'admin' } + ]) expect(User.query().count()).toBe(3) expect(User.query().where('role', 'admin').count()).toBe(2) }) it('can get max value of the specified field', async () => { - await User.create({ - data: [ - { id: 8, role: 'admin' }, - { id: 12, role: 'user' }, - { id: 11, role: 'admin' }, - { id: 'A', role: 'admin' } - ] - }) + await User.create([ + { id: 8, role: 'admin' }, + { id: 12, role: 'user' }, + { id: 11, role: 'admin' }, + { id: 'A', role: 'admin' } + ]) expect(User.query().max('id')).toBe(12) expect(User.query().max('role')).toBe(0) @@ -52,14 +48,12 @@ describe('Feature – Aggregates', () => { }) it('can get min value of the specified field', async () => { - await User.create({ - data: [ - { id: 8, role: 'admin' }, - { id: 12, role: 'user' }, - { id: 11, role: 'admin' }, - { id: 'A', role: 'admin' } - ] - }) + await User.create([ + { id: 8, role: 'admin' }, + { id: 12, role: 'user' }, + { id: 11, role: 'admin' }, + { id: 'A', role: 'admin' } + ]) expect(User.query().min('id')).toBe(8) expect(User.query().min('role')).toBe(0) @@ -67,14 +61,12 @@ describe('Feature – Aggregates', () => { }) it('can get sum value of the specified field', async () => { - await User.create({ - data: [ - { id: 8, role: 'admin' }, - { id: 12, role: 'user' }, - { id: 11, role: 'admin' }, - { id: 'A', role: 'admin' } - ] - }) + await User.create([ + { id: 8, role: 'admin' }, + { id: 12, role: 'user' }, + { id: 11, role: 'admin' }, + { id: 'A', role: 'admin' } + ]) expect(User.query().sum('id')).toBe(31) expect(User.query().where('role', 'admin').sum('id')).toBe(19) diff --git a/test/feature/attributes/Boolean.spec.ts b/test/feature/attributes/Boolean.spec.ts index b249142c..a7caab4d 100644 --- a/test/feature/attributes/Boolean.spec.ts +++ b/test/feature/attributes/Boolean.spec.ts @@ -22,18 +22,16 @@ describe('Feature – Attributes – Boolean', () => { it('casts the value to `Boolean` when creating data', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, bool: '' }, - { id: 3, bool: 'string' }, - { id: 4, bool: '0' }, - { id: 5, bool: 0 }, - { id: 6, bool: 1 }, - { id: 7, bool: true }, - { id: 8, bool: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, bool: '' }, + { id: 3, bool: 'string' }, + { id: 4, bool: '0' }, + { id: 5, bool: 0 }, + { id: 6, bool: 1 }, + { id: 7, bool: true }, + { id: 8, bool: null } + ]) const expected = createState({ users: { @@ -54,18 +52,16 @@ describe('Feature – Attributes – Boolean', () => { it('casts the value to `Boolean` when retrieving data', async () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, bool: '' }, - { id: 3, bool: 'string' }, - { id: 4, bool: '0' }, - { id: 5, bool: 0 }, - { id: 6, bool: 1 }, - { id: 7, bool: true }, - { id: 8, bool: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, bool: '' }, + { id: 3, bool: 'string' }, + { id: 4, bool: '0' }, + { id: 5, bool: 0 }, + { id: 6, bool: 1 }, + { id: 7, bool: true }, + { id: 8, bool: null } + ]) const users = User.all() @@ -99,18 +95,16 @@ describe('Feature – Attributes – Boolean', () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, bool: '' }, - { id: 3, bool: 'string' }, - { id: 4, bool: '0' }, - { id: 5, bool: 0 }, - { id: 6, bool: 1 }, - { id: 7, bool: true }, - { id: 8, bool: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, bool: '' }, + { id: 3, bool: 'string' }, + { id: 4, bool: '0' }, + { id: 5, bool: 0 }, + { id: 6, bool: 1 }, + { id: 7, bool: true }, + { id: 8, bool: null } + ]) const users = User.all() diff --git a/test/feature/attributes/Number.spec.ts b/test/feature/attributes/Number.spec.ts index 27063aeb..6c3fe19d 100644 --- a/test/feature/attributes/Number.spec.ts +++ b/test/feature/attributes/Number.spec.ts @@ -22,18 +22,16 @@ describe('Feature – Attributes – Number', () => { it('casts the value to `Number` when creating data', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, num: 1 }, - { id: 3, num: 1.5 }, - { id: 4, num: '2' }, - { id: 5, num: '2.5' }, - { id: 6, num: true }, - { id: 7, num: false }, - { id: 8, num: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, num: 1 }, + { id: 3, num: 1.5 }, + { id: 4, num: '2' }, + { id: 5, num: '2.5' }, + { id: 6, num: true }, + { id: 7, num: false }, + { id: 8, num: null } + ]) const expected = createState({ users: { @@ -54,16 +52,14 @@ describe('Feature – Attributes – Number', () => { it('casts the value to `Number` when retrieving data', async () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, num: 1 }, - { id: 3, num: '2' }, - { id: 4, num: true }, - { id: 5, num: false }, - { id: 6, num: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, num: 1 }, + { id: 3, num: '2' }, + { id: 4, num: true }, + { id: 5, num: false }, + { id: 6, num: null } + ]) const users = User.all() @@ -95,16 +91,14 @@ describe('Feature – Attributes – Number', () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, num: 1 }, - { id: 3, num: '2' }, - { id: 4, num: true }, - { id: 5, num: false }, - { id: 6, num: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, num: 1 }, + { id: 3, num: '2' }, + { id: 4, num: true }, + { id: 5, num: false }, + { id: 6, num: null } + ]) const users = User.all() diff --git a/test/feature/attributes/String.spec.ts b/test/feature/attributes/String.spec.ts index 06675cdc..1bfaf744 100644 --- a/test/feature/attributes/String.spec.ts +++ b/test/feature/attributes/String.spec.ts @@ -22,15 +22,13 @@ describe('Feature – Attributes – String', () => { it('casts the value to `String` when creating data', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, str: 'value' }, - { id: 3, str: 1 }, - { id: 4, str: true }, - { id: 5, str: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, str: 'value' }, + { id: 3, str: 1 }, + { id: 4, str: true }, + { id: 5, str: null } + ]) const expected = createState({ users: { @@ -48,15 +46,13 @@ describe('Feature – Attributes – String', () => { it('casts the value to `String` when retrieving data', async () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, str: 'value' }, - { id: 3, str: 1 }, - { id: 4, str: true }, - { id: 5, str: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, str: 'value' }, + { id: 3, str: 1 }, + { id: 4, str: true }, + { id: 5, str: null } + ]) const users = User.all() @@ -87,15 +83,13 @@ describe('Feature – Attributes – String', () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1 }, - { id: 2, str: 'value' }, - { id: 3, str: 1 }, - { id: 4, str: true }, - { id: 5, str: null } - ] - }) + await User.create([ + { id: 1 }, + { id: 2, str: 'value' }, + { id: 3, str: 1 }, + { id: 4, str: true }, + { id: 5, str: null } + ]) const users = User.all() diff --git a/test/feature/attributes/Uid.spec.ts b/test/feature/attributes/Uid.spec.ts index ddbc855c..1e752a6f 100644 --- a/test/feature/attributes/Uid.spec.ts +++ b/test/feature/attributes/Uid.spec.ts @@ -28,9 +28,7 @@ describe('Feature – Attributes – Uid', () => { it('generates uid as a default value', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: [{}, {}, {}] - }) + await User.create([{}, {}, {}]) const expected = createState({ users: { @@ -46,9 +44,10 @@ describe('Feature – Attributes – Uid', () => { it('will do nothing if the value exists', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: [{ id: 1, id2: 'id1' }, { id: 2, id2: 'id2' }] - }) + await User.create([ + { id: 1, id2: 'id1' }, + { id: 2, id2: 'id2' } + ]) const expected = createState({ users: { @@ -82,7 +81,7 @@ describe('Feature – Attributes – Uid', () => { it('generates user provided uid as a default value', async () => { const store = createStore([{ model: User }]) - await User.create({ data: [{}] }) + await User.create([{}]) const expected = createState({ users: { diff --git a/test/feature/basics/Create.spec.ts b/test/feature/basics/Create.spec.ts index b2f7c1c3..35c05156 100644 --- a/test/feature/basics/Create.spec.ts +++ b/test/feature/basics/Create.spec.ts @@ -13,10 +13,10 @@ describe('Feature – Basics – Create', () => { } } - it('can create a data', async () => { + it('can create a record', async () => { const store = createStore([{ model: User }]) - await User.create({ + await store.dispatch('entities/users/create', { data: { id: 1, name: 'John Doe' } }) @@ -28,7 +28,7 @@ describe('Feature – Basics – Create', () => { expect(store.state.entities.users.data).toEqual(expected) }) - it('can create list of data', async () => { + it('can create a list of records', async () => { const store = createStore([{ model: User }]) await store.dispatch('entities/users/create', { @@ -110,7 +110,7 @@ describe('Feature – Basics – Create', () => { expect(store.state.entities.users.data).toEqual(expected) }) - it('returns a newly created data', async () => { + it('returns a newly created record', async () => { const store = createStore([{ model: User }]) const collection = await store.dispatch('entities/users/create', { @@ -124,7 +124,7 @@ describe('Feature – Basics – Create', () => { expect(collection).toEqual(expected) }) - it('returns list of newly created data', async () => { + it('returns list of newly created record', async () => { const store = createStore([{ model: User }]) const collection = await store.dispatch('entities/users/create', { @@ -144,10 +144,12 @@ describe('Feature – Basics – Create', () => { expect(collection).toEqual(expected) }) - it('returns null when creating empty data', async () => { + it('returns null when creating an empty record', async () => { const store = createStore([{ model: User }]) - const collection = await store.dispatch('entities/users/create', { data: {} }) + const collection = await store.dispatch('entities/users/create', { + data: {} + }) expect(collection).toEqual({}) }) diff --git a/test/feature/basics/Delete.spec.ts b/test/feature/basics/Delete.spec.ts index b31d21cb..82f786b2 100644 --- a/test/feature/basics/Delete.spec.ts +++ b/test/feature/basics/Delete.spec.ts @@ -37,9 +37,7 @@ describe('Feature – Basics – Delete', () => { it('can delete a record by specifying the id', async () => { const store = getStore() - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) await User.delete(1) @@ -56,9 +54,7 @@ describe('Feature – Basics – Delete', () => { it('can delete a record by specifying id as a string', async () => { const store = getStore() - await User.create({ - data: [{ id: 'string_id_1' }, { id: 'string_id_2' }] - }) + await User.create([{ id: 'string_id_1' }, { id: 'string_id_2' }]) await User.delete('string_id_2') @@ -75,13 +71,11 @@ describe('Feature – Basics – Delete', () => { it('can delete records by specifying a closure', async () => { const store = getStore() - await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Jane Doe' } - ] - }) + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + { id: 3, name: 'Jane Doe' } + ]) await User.delete(user => user.name === 'Jane Doe') @@ -98,9 +92,7 @@ describe('Feature – Basics – Delete', () => { it('does nothing if the specified id does not exist', async () => { const store = getStore() - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) await User.delete(3) @@ -118,9 +110,7 @@ describe('Feature – Basics – Delete', () => { it('returns deleted item', async () => { const store = getStore() - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) const user = await User.delete(1) @@ -140,13 +130,11 @@ describe('Feature – Basics – Delete', () => { it('returns all deleted records as a collection when specifying a closure', async () => { const store = getStore() - await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Jane Doe' } - ] - }) + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + { id: 3, name: 'Jane Doe' } + ]) const users = await User.delete(user => user.name === 'Jane Doe') @@ -169,12 +157,10 @@ describe('Feature – Basics – Delete', () => { it('deletes itself by instance method', async () => { const store = getStore() - await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const user = User.find(2) as User @@ -193,9 +179,7 @@ describe('Feature – Basics – Delete', () => { it('can delete all records in the entity', async () => { const store = getStore() - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) await User.deleteAll() @@ -210,9 +194,7 @@ describe('Feature – Basics – Delete', () => { it('can delete all records in the entity by the instance method', async () => { const store = getStore() - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) await (new User()).$deleteAll() @@ -227,13 +209,9 @@ describe('Feature – Basics – Delete', () => { it('can delete all records in the entire entities', async () => { const store = getStore() - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) - await Post.create({ - data: [{ id: 3 }, { id: 4 }] - }) + await Post.create([{ id: 3 }, { id: 4 }]) await store.dispatch('entities/deleteAll') diff --git a/test/feature/basics/Delete_Composite_Key.spec.ts b/test/feature/basics/Delete_Composite_Key.spec.ts index 8ffb6f08..2cfd4596 100644 --- a/test/feature/basics/Delete_Composite_Key.spec.ts +++ b/test/feature/basics/Delete_Composite_Key.spec.ts @@ -20,9 +20,10 @@ describe('Feature – Basics – Delete Composite Key', () => { it('can delete a record by specifying the composite id as an array', async () => { const store = getStore() - await User.create({ - data: [{ first_id: 1, second_id: 1 }, { first_id: 1, second_id: 2 }] - }) + await User.create([ + { first_id: 1, second_id: 1 }, + { first_id: 1, second_id: 2 } + ]) await User.delete([1, 1]) @@ -38,12 +39,10 @@ describe('Feature – Basics – Delete Composite Key', () => { it('deletes itself by instance method even when model has composite primary key', async () => { const store = getStore() - await User.create({ - data: [ - { first_id: 1, second_id: 2 }, - { first_id: 3, second_id: 4 } - ] - }) + await User.create([ + { first_id: 1, second_id: 2 }, + { first_id: 3, second_id: 4 } + ]) const user = User.find([1, 2]) as User diff --git a/test/feature/basics/Exists.spec.ts b/test/feature/basics/Exists.spec.ts index 3d43fd60..ac9c7bf9 100644 --- a/test/feature/basics/Exists.spec.ts +++ b/test/feature/basics/Exists.spec.ts @@ -19,21 +19,17 @@ describe('Feature – Exists', () => { }) it('can check if a database contains data by exists method', async () => { - const store = createStore([{ model: User }]) + createStore([{ model: User }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) expect(User.exists()).toBe(true) }) it('can check if a query chain would return data', async () => { - const store = createStore([{ model: User }]) + createStore([{ model: User }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) expect(User.query().where('id', 3).exists()).toBe(false) expect(User.query().where('id', 1).exists()).toBe(true) diff --git a/test/feature/basics/Insert.spec.ts b/test/feature/basics/Insert.spec.ts index f3ef7e10..b9fb26d0 100644 --- a/test/feature/basics/Insert.spec.ts +++ b/test/feature/basics/Insert.spec.ts @@ -18,7 +18,7 @@ describe('Feature – Basics – Insert', () => { it('can insert a record', async () => { const store = getStore() - await User.insert({ + await store.dispatch('entities/users/insert', { data: { id: 1, name: 'John Doe' } }) @@ -34,7 +34,7 @@ describe('Feature – Basics – Insert', () => { it('does nothing if an empty object is passed', async () => { const store = getStore() - await User.insert({ + await store.dispatch('entities/users/insert', { data: {} }) @@ -48,7 +48,7 @@ describe('Feature – Basics – Insert', () => { it('can insert record with primary key value of `null`', async () => { const store = getStore() - await User.insert({ + await store.dispatch('entities/users/insert', { data: { id: null, name: 'John Doe' } }) diff --git a/test/feature/basics/PersistOptions.spec.ts b/test/feature/basics/PersistOptions.spec.ts index cc0fb2be..ac5a51bc 100644 --- a/test/feature/basics/PersistOptions.spec.ts +++ b/test/feature/basics/PersistOptions.spec.ts @@ -1,7 +1,7 @@ import { createStore } from 'test/support/Helpers' import Model from '@/model/Model' -describe('Feature – Basics – Create', () => { +describe('Feature – Basics – Persist Options', () => { class User extends Model { static entity = 'users' @@ -28,21 +28,17 @@ describe('Feature – Basics – Create', () => { it('can `create` related records', async () => { const store = createStore([{ model: User }, { model: Post }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Post.create({ - data: { id: 1 } - }) + await Post.create({ id: 1 }) - await User.insert({ - data: { + await User.insert( + { id: 2, posts: [{ id: 2 }, { id: 3 }] }, - create: ['posts'] - }) + { create: ['posts'] } + ) expect(store.state.entities.users.data['1'].id).toBe(1) expect(store.state.entities.users.data['2'].id).toBe(2) @@ -54,21 +50,17 @@ describe('Feature – Basics – Create', () => { it('can `insert` related records', async () => { const store = createStore([{ model: User }, { model: Post }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Post.create({ - data: { id: 1 } - }) + await Post.create({ id: 1 }) - await User.create({ - data: { + await User.create( + { id: 2, posts: [{ id: 2 }, { id: 3 }] }, - insert: ['posts'] - }) + { insert: ['posts'] } + ) expect(store.state.entities.users.data['1']).toBe(undefined) expect(store.state.entities.users.data['2'].id).toBe(2) @@ -80,27 +72,23 @@ describe('Feature – Basics – Create', () => { it('can `update` related records', async () => { const store = createStore([{ model: User }, { model: Post }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Post.create({ - data: [ - { id: 1, title: 'Title 01' }, - { id: 2, title: 'Title 02' } - ] - }) + await Post.create([ + { id: 1, title: 'Title 01' }, + { id: 2, title: 'Title 02' } + ]) - await User.create({ - data: { + await User.create( + { id: 2, posts: [ { id: 1, title: 'Title 01-Edit' }, { id: 2, title: 'Title 02-Edit' } ] }, - update: ['posts'] - }) + { update: ['posts'] } + ) expect(store.state.entities.users.data['1']).toBe(undefined) expect(store.state.entities.users.data['2'].id).toBe(2) @@ -113,27 +101,23 @@ describe('Feature – Basics – Create', () => { it('can `insertOrUpdate` related records', async () => { const store = createStore([{ model: User }, { model: Post }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Post.create({ - data: [ - { id: 1, title: 'Title 01' }, - { id: 2, title: 'Title 02' } - ] - }) + await Post.create([ + { id: 1, title: 'Title 01' }, + { id: 2, title: 'Title 02' } + ]) - await User.create({ - data: { + await User.create( + { id: 2, posts: [ { id: 2, title: 'Title 02-Edit' }, { id: 3, title: 'Title 03' } ] }, - insertOrUpdate: ['posts'] - }) + { insertOrUpdate: ['posts'] } + ) expect(store.state.entities.users.data['1']).toBe(undefined) expect(store.state.entities.users.data['2'].id).toBe(2) diff --git a/test/feature/basics/Retrieve.spec.ts b/test/feature/basics/Retrieve.spec.ts index 191f719d..b1448d4c 100644 --- a/test/feature/basics/Retrieve.spec.ts +++ b/test/feature/basics/Retrieve.spec.ts @@ -124,23 +124,23 @@ describe('Feature – Retrieve', () => { describe('#first', () => { it('can retrieve the first item from the store', async () => { - createStore([{ model: User }]) + const store = createStore([{ model: User }]) - await User.insert({ + await store.dispatch('entities/users/insert', { data: [{ id: 1 }, { id: 2 }] }) const expected = { $id: '1', id: 1 } - const user = User.query().first() + const user = store.getters['entities/users/query']().first() expect(user).toEqual(expected) }) it('returns `null` if it can not find any record', () => { - createStore([{ model: User }]) + const store = createStore([{ model: User }]) - const user = User.query().first() + const user = store.getters['entities/users/query']().first() expect(user).toBe(null) }) @@ -148,23 +148,23 @@ describe('Feature – Retrieve', () => { describe('#last', () => { it('can retrieve the last item from the store', async () => { - createStore([{ model: User }]) + const store = createStore([{ model: User }]) - await User.insert({ + await store.dispatch('entities/users/insert', { data: [{ id: 1 }, { id: 2 }] }) const expected = { $id: '2', id: 2 } - const user = User.query().last() + const user = store.getters['entities/users/query']().last() expect(user).toEqual(expected) }) it('returns `null` if it can not find any record', () => { - createStore([{ model: User }]) + const store = createStore([{ model: User }]) - const user = User.query().last() + const user = store.getters['entities/users/query']().last() expect(user).toBe(null) }) diff --git a/test/feature/basics/Update.spec.ts b/test/feature/basics/Update.spec.ts index d6c2c76a..30525120 100644 --- a/test/feature/basics/Update.spec.ts +++ b/test/feature/basics/Update.spec.ts @@ -24,15 +24,17 @@ describe('Feature – Basics – Update', () => { } } - createStore([{ model: User }]) + const store = createStore([{ model: User }]) - await User.create({ + await store.dispatch('entities/users/create', { data: { id: 0, name: 'John Doe', age: 30 } }) - await User.update({ id: 0, age: 24 }) + await store.dispatch('entities/users/update', { + data: { id: 0, age: 24 } + }) - const user = User.find(0) as User + const user = store.getters['entities/users/find'](0) expect(user.name).toBe('John Doe') expect(user.age).toBe(24) @@ -606,13 +608,13 @@ describe('Feature – Basics – Update', () => { } } - createStore([{ model: User }]) + const store = createStore([{ model: User }]) - await User.create({ + await store.dispatch('entities/users/create', { data: { id: 1, name: 'John Doe' } }) - await User.update({ + await store.dispatch('entities/users/update', { where: 1, data: { id: 2 } }) diff --git a/test/feature/hooks/Global.spec.ts b/test/feature/hooks/Global.spec.ts index 729d9f12..6b14fa88 100644 --- a/test/feature/hooks/Global.spec.ts +++ b/test/feature/hooks/Global.spec.ts @@ -28,9 +28,11 @@ describe('Feature – Hooks – Global', () => { createStore([{ model: User }]) - await User.create({ - data: [{ id: 1, role: 'admin' }, { id: 2, role: 'admin' }, { id: 3, role: 'user' }] - }) + await User.create([ + { id: 1, role: 'admin' }, + { id: 2, role: 'admin' }, + { id: 3, role: 'user' } + ]) const expected = [{ $id: '1', id: 1, role: 'admin' }] @@ -82,6 +84,7 @@ describe('Feature – Hooks – Global', () => { const persistedHookId1 = Query.on('afterWhere', callbackFunction) expect(Query.hooks.afterWhere.length).toBe(1) + User.all() expect(Query.hooks.afterWhere.length).toBe(1) diff --git a/test/feature/hooks/Global_Create.spec.ts b/test/feature/hooks/Global_Create.spec.ts index 5a7de672..bca8ef21 100644 --- a/test/feature/hooks/Global_Create.spec.ts +++ b/test/feature/hooks/Global_Create.spec.ts @@ -32,9 +32,7 @@ describe('Feature – Hooks – Global Create', () => { model.role = 'admin' }) - await User.create({ - data: [{ id: 1, role: 'admin' }, { id: 2, role: 'user' }] - }) + await User.create([{ id: 1, role: 'admin' }, { id: 2, role: 'user' }]) const users = User.all() @@ -46,9 +44,7 @@ describe('Feature – Hooks – Global Create', () => { Query.on('beforeCreate', (model: any, _entity: any) => { model.role = 'admin' }) Query.on('beforeCreate', (model: any, _entity: any) => { model.role = 'not admin' }) - await User.create({ - data: [{ id: 1, role: 'admin' }, { id: 2, role: 'user' }] - }) + await User.create([{ id: 1, role: 'admin' }, { id: 2, role: 'user' }]) const users = User.all() @@ -63,9 +59,7 @@ describe('Feature – Hooks – Global Create', () => { } }) - await User.create({ - data: [{ id: 1, role: 'admin' }, { id: 2, role: 'user' }] - }) + await User.create([{ id: 1, role: 'admin' }, { id: 2, role: 'user' }]) const users = User.all() @@ -80,9 +74,7 @@ describe('Feature – Hooks – Global Create', () => { hit = true }) - await User.create({ - data: { id: 1, role: 'admin' } - }) + await User.create({ id: 1, role: 'admin' }) expect(hit).toBe(true) }) diff --git a/test/feature/hooks/Global_Delete.spec.ts b/test/feature/hooks/Global_Delete.spec.ts index 47cc77b4..167d72c1 100644 --- a/test/feature/hooks/Global_Delete.spec.ts +++ b/test/feature/hooks/Global_Delete.spec.ts @@ -34,9 +34,7 @@ describe('Feature – Hooks – Global Delete', () => { hit = true }) - await User.create({ - data: { id: 1, role: 'user' } - }) + await User.create({ id: 1, role: 'user' }) await User.delete(1) @@ -51,9 +49,7 @@ describe('Feature – Hooks – Global Delete', () => { return false }) - await User.create({ - data: { id: 1, role: 'user' } - }) + await User.create({ id: 1, role: 'user' }) await User.delete(1) @@ -69,9 +65,7 @@ describe('Feature – Hooks – Global Delete', () => { hit = true }) - await User.create({ - data: { id: 1, role: 'user' } - }) + await User.create({ id: 1, role: 'user' }) await User.delete(1) diff --git a/test/feature/hooks/Global_Update.spec.ts b/test/feature/hooks/Global_Update.spec.ts index 157c9491..9cf99358 100644 --- a/test/feature/hooks/Global_Update.spec.ts +++ b/test/feature/hooks/Global_Update.spec.ts @@ -32,13 +32,9 @@ describe('Feature – Hooks – Global Update', () => { model.role = 'admin' }) - await User.create({ - data: { id: 1, role: 'user' } - }) + await User.create({ id: 1, role: 'user' }) - await User.update({ - data: { id: 1, role: 'guest' } - }) + await User.update({ id: 1, role: 'guest' }) const user = User.find(1) @@ -52,13 +48,9 @@ describe('Feature – Hooks – Global Update', () => { } }) - await User.create({ - data: { id: 1, role: 'user' } - }) + await User.create({ id: 1, role: 'user' }) - await User.update({ - data: { id: 1, role: 'admin' } - }) + await User.update({ id: 1, role: 'admin' }) const user = User.find(1) @@ -72,13 +64,9 @@ describe('Feature – Hooks – Global Update', () => { hit = true }) - await User.create({ - data: { id: 1, role: 'user' } - }) + await User.create({ id: 1, role: 'user' }) - await User.update({ - data: { id: 1, role: 'admin' } - }) + await User.update({ id: 1, role: 'admin' }) expect(hit).toBe(true) }) diff --git a/test/feature/hooks/Local_Binding.spec.ts b/test/feature/hooks/Local_Binding.spec.ts index 3db1e0e3..e0bb8f11 100644 --- a/test/feature/hooks/Local_Binding.spec.ts +++ b/test/feature/hooks/Local_Binding.spec.ts @@ -34,9 +34,7 @@ describe('Hooks – Local Binding', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) const expected = { $id: '1', id: 1, name: 'John Doe', age: 30 } diff --git a/test/feature/hooks/Local_Create.spec.ts b/test/feature/hooks/Local_Create.spec.ts index aa636902..56a8c79d 100644 --- a/test/feature/hooks/Local_Create.spec.ts +++ b/test/feature/hooks/Local_Create.spec.ts @@ -30,9 +30,7 @@ describe('Hooks – Local Create', () => { createStore([{ model: User }]) - await User.create({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.create({ id: 1, name: 'John Doe', age: 20 }) const expected = { $id: '1', id: 1, name: 'John Doe', age: 30 } @@ -67,9 +65,7 @@ describe('Hooks – Local Create', () => { createStore([{ model: User }]) - await User.create({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.create({ id: 1, name: 'John Doe', age: 20 }) const expected = { $id: '1', id: 1, name: 'John Doe', age: 20 } @@ -106,12 +102,10 @@ describe('Hooks – Local Create', () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1, name: 'John Doe', age: 20 }, - { id: 2, name: 'Jane Doe', age: 24 } - ] - }) + await User.create([ + { id: 1, name: 'John Doe', age: 20 }, + { id: 2, name: 'Jane Doe', age: 24 } + ]) const users = User.all() @@ -148,8 +142,6 @@ describe('Hooks – Local Create', () => { createStore([{ model: User }]) - await User.create({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.create({ id: 1, name: 'John Doe', age: 20 }) }) }) diff --git a/test/feature/hooks/Local_Delete.spec.ts b/test/feature/hooks/Local_Delete.spec.ts index c5f463c0..b20a143c 100644 --- a/test/feature/hooks/Local_Delete.spec.ts +++ b/test/feature/hooks/Local_Delete.spec.ts @@ -28,9 +28,7 @@ describe('Hooks – Local Delete', () => { createStore([{ model: User }]) - await User.create({ - data: { id: 1, name: 'John Doe' } - }) + await User.create({ id: 1, name: 'John Doe' }) await User.delete(1) @@ -65,12 +63,10 @@ describe('Hooks – Local Delete', () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) await User.delete(2) @@ -109,12 +105,10 @@ describe('Hooks – Local Delete', () => { createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) await User.delete(2) diff --git a/test/feature/hooks/Local_Insert.spec.ts b/test/feature/hooks/Local_Insert.spec.ts index 4467729b..895e7de7 100644 --- a/test/feature/hooks/Local_Insert.spec.ts +++ b/test/feature/hooks/Local_Insert.spec.ts @@ -30,9 +30,7 @@ describe('Hooks – Local Insert', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) const expected = { $id: '1', id: 1, name: 'John Doe', age: 30 } @@ -67,9 +65,7 @@ describe('Hooks – Local Insert', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) const expected = { $id: '1', id: 1, name: 'John Doe', age: 20 } @@ -148,8 +144,6 @@ describe('Hooks – Local Insert', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) }) }) diff --git a/test/feature/hooks/Local_Update.spec.ts b/test/feature/hooks/Local_Update.spec.ts index 0c62e228..130bbde0 100644 --- a/test/feature/hooks/Local_Update.spec.ts +++ b/test/feature/hooks/Local_Update.spec.ts @@ -30,9 +30,7 @@ describe('Hooks – Local Update', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) await User.update({ id: 1, name: 'Jane Doe' }) @@ -69,9 +67,7 @@ describe('Hooks – Local Update', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) await User.update({ id: 1, name: 'Jane Doe', age: 30 }) @@ -108,12 +104,10 @@ describe('Hooks – Local Update', () => { createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe', age: 20 }, - { id: 2, name: 'Jane Doe', age: 24 } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe', age: 20 }, + { id: 2, name: 'Jane Doe', age: 24 } + ]) await User.update({ id: 1, name: 'Johnny Doe' }) @@ -158,9 +152,7 @@ describe('Hooks – Local Update', () => { createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe', age: 20 } - }) + await User.insert({ id: 1, name: 'John Doe', age: 20 }) await User.update({ id: 1, age: 30 }) diff --git a/test/feature/inheritance/Inheritance.spec.ts b/test/feature/inheritance/Inheritance.spec.ts index 483a936c..bb5bafee 100644 --- a/test/feature/inheritance/Inheritance.spec.ts +++ b/test/feature/inheritance/Inheritance.spec.ts @@ -63,9 +63,7 @@ describe('Feature – Inheritance', () => { it('sets right class for derived entity', async () => { createStore([Person, Adult]) - await Adult.insert({ - data: { id: 1, name: 'John Doe' } - }) + await Adult.insert({ id: 1, name: 'John Doe' }) expect(Adult.find(1)).toBeInstanceOf(Adult) }) @@ -73,9 +71,7 @@ describe('Feature – Inheritance', () => { it('should inherit fields from root entity', async () => { createStore([Person, Adult]) - await Adult.insert({ - data: { id: 2, name: 'John Doe' } - }) + await Adult.insert({ id: 2, name: 'John Doe' }) const adult = Adult.find(2) as Adult @@ -86,13 +82,9 @@ describe('Feature – Inheritance', () => { it('should have own fields only on derived entity', async () => { createStore([Person, Adult]) - await Person.insert({ - data: { id: 1, name: 'John' } - }) + await Person.insert({ id: 1, name: 'John' }) - await Adult.insert({ - data: { id: 2, name: 'Jane', job: 'Software Engineer' } - }) + await Adult.insert({ id: 2, name: 'Jane', job: 'Software Engineer' }) const person = Person.find(1) as any expect(person.job).toBe(undefined) diff --git a/test/feature/inheritance/Inheritance_CRUD.spec.ts b/test/feature/inheritance/Inheritance_CRUD.spec.ts index 9eb8d653..80898f5a 100644 --- a/test/feature/inheritance/Inheritance_CRUD.spec.ts +++ b/test/feature/inheritance/Inheritance_CRUD.spec.ts @@ -56,13 +56,9 @@ describe('Feature - Inheritance - CRUD', () => { it('can get mixed results when calling getter of root entity', async () => { createStore([Person, Adult]) - await Person.insert({ - data: { id: 1, name: 'John' } - }) + await Person.insert({ id: 1, name: 'John' }) - await Adult.insert({ - data: { id: 2, name: 'Jane', job: 'Software Engineer' } - }) + await Adult.insert({ id: 2, name: 'Jane', job: 'Software Engineer' }) const people = Person.query().orderBy('id').get() @@ -79,13 +75,9 @@ describe('Feature - Inheritance - CRUD', () => { it('can get only derived results when calling getter of derived entity', async () => { createStore([Person, Adult]) - await Person.insert({ - data: { id: 1, name: 'John' } - }) + await Person.insert({ id: 1, name: 'John' }) - await Adult.insert({ - data: { id: 2, name: 'Jane', job: 'Software Engineer' } - }) + await Adult.insert({ id: 2, name: 'Jane', job: 'Software Engineer' }) const adults = Adult.all() @@ -98,19 +90,15 @@ describe('Feature - Inheritance - CRUD', () => { it('should clean only corresponding entities (and derived ones) when calling create', async () => { createStore([Person, Adult, Child ]) - await Person.insert({ - data: [ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' }, - { id: 3, name: 'D', type: 'CHILD' } - ] - }) + await Person.insert([ + { id: 1, name: 'A', type: 'PERSON' }, + { id: 2, name: 'B', type: 'ADULT' }, + { id: 3, name: 'D', type: 'CHILD' } + ]) expect(Person.all().length).toBe(3) - await Adult.create({ - data: { id: 4, name: 'E' } - }) + await Adult.create({ id: 4, name: 'E' }) expect(Person.all().length).toBe(3) @@ -123,19 +111,14 @@ describe('Feature - Inheritance - CRUD', () => { it('should update derived data without changing its class when calling base update', async () => { createStore([Person, Adult]) - await Person.create({ - data: [ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ] - }) + await Person.create([ + { id: 1, name: 'A', type: 'PERSON' }, + { id: 2, name: 'B', type: 'ADULT' } + ]) expect((Person.find(2) as Adult).name).toBe('B') - await Person.update({ - where: 2, - data: { name: 'C' } - }) + await Person.update({ name: 'C' }, { where: 2 }) const adult = Person.find(2) as Adult @@ -146,17 +129,15 @@ describe('Feature - Inheritance - CRUD', () => { it('should update data with a closure using instanceof', async () => { createStore([Person, Adult]) - await Person.create({ - data: [ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ] - }) + await Person.create([ + { id: 1, name: 'A', type: 'PERSON' }, + { id: 2, name: 'B', type: 'ADULT' } + ]) - await Person.update({ - where: (record) => record instanceof Adult, - data: { name: 'C' } - }) + await Person.update( + { name: 'C' }, + { where: (record) => record instanceof Adult } + ) const adult = Person.find(2) as Adult @@ -167,17 +148,13 @@ describe('Feature - Inheritance - CRUD', () => { it('should update or insert mixed data', async () => { const store = createStore([Person, Adult]) - await Person.create({ - data: { id: 1, name: 'John Doe', job: 'Software Engineer', type: 'ADULT' } - }) + await Person.create({ id: 1, name: 'John Doe', job: 'Software Engineer', type: 'ADULT' }) - await Person.insertOrUpdate({ - data: [ - { id: 1, name: 'John Doe', job: 'Writer', type: 'ADULT' }, - { id: 2, name: 'Jane Doe', job: 'QA', type: 'ADULT' }, - { id: 3, name: 'Jane Doe', type: 'PERSON' } - ] - }) + await Person.insertOrUpdate([ + { id: 1, name: 'John Doe', job: 'Writer', type: 'ADULT' }, + { id: 2, name: 'Jane Doe', job: 'QA', type: 'ADULT' }, + { id: 3, name: 'Jane Doe', type: 'PERSON' } + ]) const expected = createState({ person: { @@ -194,17 +171,15 @@ describe('Feature - Inheritance - CRUD', () => { it('should update only derived data when field is a derived entity field', async () => { createStore([Person, Adult]) - await Person.create({ - data: [ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', job: 'Software Engineer', type: 'ADULT' } - ] - }) + await Person.create([ + { id: 1, name: 'A', type: 'PERSON' }, + { id: 2, name: 'B', job: 'Software Engineer', type: 'ADULT' } + ]) - await Person.update({ - where: (record) => record instanceof Person, - data: { job: 'Writer' } - }) + await Person.update( + { job: 'Writer' }, + { where: (record) => record instanceof Person } + ) const person = Person.find(1) as any expect(person.job).toBe(undefined) @@ -216,12 +191,10 @@ describe('Feature - Inheritance - CRUD', () => { it('should delete record correctly when manipulating a derived entity', async () => { createStore([Person, Adult]) - await Person.create({ - data: [ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ] - }) + await Person.create([ + { id: 1, name: 'A', type: 'PERSON' }, + { id: 2, name: 'B', type: 'ADULT' } + ]) expect(Person.all().length).toBe(2) @@ -236,12 +209,10 @@ describe('Feature - Inheritance - CRUD', () => { it('should delete only derived records when calling deleteAll on derived entity', async () => { createStore([Person, Adult]) - await Person.create({ - data: [ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ] - }) + await Person.create([ + { id: 1, name: 'A', type: 'PERSON' }, + { id: 2, name: 'B', type: 'ADULT' } + ]) expect(Person.all().length).toBe(2) diff --git a/test/feature/inheritance/Inheritance_Discriminator.spec.ts b/test/feature/inheritance/Inheritance_Discriminator.spec.ts index 80cff44f..d23f931e 100644 --- a/test/feature/inheritance/Inheritance_Discriminator.spec.ts +++ b/test/feature/inheritance/Inheritance_Discriminator.spec.ts @@ -28,9 +28,7 @@ describe('Feature - Inheritance - Discriminator field', () => { createStore([Person, Adult]) - await Person.insert({ - data: { id: 1, name: 'John Doe', type: 'ADULT' } - }) + await Person.insert({ id: 1, name: 'John Doe', type: 'ADULT' }) expect(Adult.find(1)).toBeInstanceOf(Adult) }) @@ -64,9 +62,7 @@ describe('Feature - Inheritance - Discriminator field', () => { createStore([Person, Adult]) - await Person.insert({ - data: { id: 1, name: 'John Doe', the_key: 'ADULT' } - }) + await Person.insert({ id: 1, name: 'John Doe', the_key: 'ADULT' }) expect(Adult.find(1)).toBeInstanceOf(Adult) }) @@ -104,14 +100,12 @@ describe('Feature - Inheritance - Discriminator field', () => { createStore([Person, Adult, Child ]) - await Person.insert({ - data: [ - { id: 1, name: 'John Doe', type: 'ADULT' }, - { id: 2, name: 'John Doe Jr', type: 'CHILD' }, - { id: 3, name: 'Doe', type: 'PERSON' }, - { id: 4, name: 'Jane Doe', type: 'ADULT' } - ] - }) + await Person.insert([ + { id: 1, name: 'John Doe', type: 'ADULT' }, + { id: 2, name: 'John Doe Jr', type: 'CHILD' }, + { id: 3, name: 'Doe', type: 'PERSON' }, + { id: 4, name: 'Jane Doe', type: 'ADULT' } + ]) const adults = Adult.all() expect(adults.length).toBe(2) @@ -155,9 +149,7 @@ describe('Feature - Inheritance - Discriminator field', () => { createStore([Person, Adult]) // Inserting through "adult" model. - await Adult.create({ - data: { id: 1, name: 'John Doe' } - }) + await Adult.create({ id: 1, name: 'John Doe' }) const adult = Adult.find(1) as Adult expect(adult.type).toBe('ADULT') @@ -193,12 +185,10 @@ describe('Feature - Inheritance - Discriminator field', () => { createStore([Person, Adult]) - await Person.create({ - data: [ - { id: 1, name: 'John Doe', type: 1 }, - { id: 2, name: 'Person Doe', type: 0 } - ] - }) + await Person.create([ + { id: 1, name: 'John Doe', type: 1 }, + { id: 2, name: 'Person Doe', type: 0 } + ]) const person = Person.find(2) as Person expect(person.name).toBe('Person Doe') diff --git a/test/feature/inheritance/Inheritance_Relations.spec.ts b/test/feature/inheritance/Inheritance_Relations.spec.ts index 9c6d23ed..ffb44154 100644 --- a/test/feature/inheritance/Inheritance_Relations.spec.ts +++ b/test/feature/inheritance/Inheritance_Relations.spec.ts @@ -54,16 +54,12 @@ describe('Feature - Inheritance - Relations', () => { createStore([Person, Adult, Job]) - await Job.insert({ - data: { id: 1, title: 'Software Engineer', adult_id: 2 } - }) + await Job.insert({ id: 1, title: 'Software Engineer', adult_id: 2 }) - await Person.insert({ - data: [ - { id: 1, name: 'John', type: 'PERSON' }, - { id: 2, name: 'Jane', type: 'ADULT' } - ] - }) + await Person.insert([ + { id: 1, name: 'John', type: 'PERSON' }, + { id: 2, name: 'Jane', type: 'ADULT' } + ]) const adult = Adult.query().with('jobs').all() @@ -126,16 +122,12 @@ describe('Feature - Inheritance - Relations', () => { createStore([Person, Adult, Job]) - await Job.insert({ - data: { id: 1, title: 'Software Engineer', adult_id: 2 } - }) + await Job.insert({ id: 1, title: 'Software Engineer', adult_id: 2 }) - await Person.insert({ - data: [ - { id: 1, name: 'John', type: 'PERSON' }, - { id: 2, name: 'Jane', type: 'ADULT' } - ] - }) + await Person.insert([ + { id: 1, name: 'John', type: 'PERSON' }, + { id: 2, name: 'Jane', type: 'ADULT' } + ]) const job = Job.query().with('adult').all() @@ -196,19 +188,15 @@ describe('Feature - Inheritance - Relations', () => { createStore([Person, Adult, Role]) - await Role.insert({ - data: [ - { id: 1, roleName: 'Role 1' }, - { id: 2, roleName: 'Role 2' } - ] - }) + await Role.insert([ + { id: 1, roleName: 'Role 1' }, + { id: 2, roleName: 'Role 2' } + ]) - await Person.insert({ - data: [ - { id: 1, name: 'John', type: 'PERSON', role_id: 1 }, - { id: 2, name: 'Jane', type: 'ADULT', role_id: 1 } - ] - }) + await Person.insert([ + { id: 1, name: 'John', type: 'PERSON', role_id: 1 }, + { id: 2, name: 'Jane', type: 'ADULT', role_id: 1 } + ]) const roles = Role.query().with('people').all() expect(roles.length).toBe(2) @@ -269,19 +257,15 @@ describe('Feature - Inheritance - Relations', () => { createStore([Person, Adult, Role]) - await Role.insert({ - data: [ - { id: 1, roleName: 'Role 1' }, - { id: 2, roleName: 'Role 2' } - ] - }) + await Role.insert([ + { id: 1, roleName: 'Role 1' }, + { id: 2, roleName: 'Role 2' } + ]) - await Person.insert({ - data: [ - { id: 1, name: 'John', type: 'PERSON', role_id: 1 }, - { id: 2, name: 'Jane', type: 'ADULT', role_id: 2 } - ] - }) + await Person.insert([ + { id: 1, name: 'John', type: 'PERSON', role_id: 1 }, + { id: 2, name: 'Jane', type: 'ADULT', role_id: 2 } + ]) // Reverse check: getting all people and their associated role. const people = Person.query().with('role').all() @@ -352,13 +336,11 @@ describe('Feature - Inheritance - Relations', () => { createStore([Person, Adult, Child]) - await Person.insert({ - data: [ - { id: 1, name: 'John', type: 'ADULT', child_id: 3 }, - { id: 2, name: 'Jane', type: 'ADULT', child_id: 3 }, - { id: 3, name: 'Jane', type: 'CHILD' } - ] - }) + await Person.insert([ + { id: 1, name: 'John', type: 'ADULT', child_id: 3 }, + { id: 2, name: 'Jane', type: 'ADULT', child_id: 3 }, + { id: 3, name: 'Jane', type: 'CHILD' } + ]) const adults = Adult.query().with('child').all() expect(adults.length).toBe(2) @@ -421,16 +403,12 @@ describe('Feature - Inheritance - Relations', () => { createStore([Person, Adult, Job]) - await Job.insert({ - data: { id: 1, title: 'Software Engineer', adult_id: 2 } - }) + await Job.insert({ id: 1, title: 'Software Engineer', adult_id: 2 }) - await Person.insert({ - data: [ - { id: 1, name: 'John', type: 'PERSON' }, - { id: 2, name: 'Jane', type: 'ADULT' } - ] - }) + await Person.insert([ + { id: 1, name: 'John', type: 'PERSON' }, + { id: 2, name: 'Jane', type: 'ADULT' } + ]) const persons = Person.query().with(['jobs', 'dummy']).all() expect(persons.length).toBe(2) diff --git a/test/feature/models/All.spec.ts b/test/feature/models/All.spec.ts index 7af71a3c..4868933b 100644 --- a/test/feature/models/All.spec.ts +++ b/test/feature/models/All.spec.ts @@ -16,12 +16,10 @@ describe('Feature – Models – All', () => { it('can fetch all records via static method', async () => { createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const users = User.all() @@ -37,12 +35,10 @@ describe('Feature – Models – All', () => { it('can fetch all records via instance method', async () => { createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const user = new User() diff --git a/test/feature/models/Create.spec.ts b/test/feature/models/Create.spec.ts index 31153d28..69b9e32c 100644 --- a/test/feature/models/Create.spec.ts +++ b/test/feature/models/Create.spec.ts @@ -17,9 +17,7 @@ describe('Feature – Models – Create', () => { it('can create a record via static method', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: { id: 1, name: 'John Doe' } - }) + await User.create({ id: 1, name: 'John Doe' }) const expected = createState({ users: { @@ -30,15 +28,13 @@ describe('Feature – Models – Create', () => { expect(store.state.entities).toEqual(expected) }) - it('can create list of record via static method', async () => { + it('can create a list of record via static method', async () => { const store = createStore([{ model: User }]) - await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const expected = createState({ users: { @@ -53,12 +49,10 @@ describe('Feature – Models – Create', () => { it('returns created records via static method', async () => { createStore([{ model: User }]) - const data = await User.create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + const data = await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) expect(data.users.length).toBe(2) expect(data.users[0]).toBeInstanceOf(User) @@ -69,9 +63,7 @@ describe('Feature – Models – Create', () => { const user = new User() - await user.$create({ - data: { id: 1, name: 'John Doe' } - }) + await user.$create({ id: 1, name: 'John Doe' }) const expected = createState({ users: { @@ -82,17 +74,15 @@ describe('Feature – Models – Create', () => { expect(store.state.entities).toEqual(expected) }) - it('can create list of record via instance method', async () => { + it('can create a list of record via instance method', async () => { const store = createStore([{ model: User }]) const user = new User() - await user.$create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await user.$create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const expected = createState({ users: { @@ -109,12 +99,10 @@ describe('Feature – Models – Create', () => { const user = new User() - const data = await user.$create({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + const data = await user.$create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) expect(data.users.length).toBe(2) expect(data.users[0]).toBeInstanceOf(User) @@ -204,9 +192,7 @@ describe('Feature – Models – Create', () => { expect(data).toEqual(dataBefore) - await Author.create({ - data - }) + await Author.create(data) const expected = createState({ authors: { diff --git a/test/feature/models/Insert.spec.ts b/test/feature/models/Insert.spec.ts index cd2993be..3a084139 100644 --- a/test/feature/models/Insert.spec.ts +++ b/test/feature/models/Insert.spec.ts @@ -17,13 +17,9 @@ describe('Feature – Models – Insert', () => { it('can insert a record via static method', async () => { const store = createStore([{ model: User }]) - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) + await User.insert({ id: 1, name: 'John Doe' }) - await User.insert({ - data: { id: 2, name: 'Jane Doe' } - }) + await User.insert({ id: 2, name: 'Jane Doe' }) const expected = createState({ users: { @@ -35,15 +31,13 @@ describe('Feature – Models – Insert', () => { expect(store.state.entities).toEqual(expected) }) - it('can insert list of record via static method', async () => { + it('can insert a list of records via static method', async () => { const store = createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const expected = createState({ users: { @@ -58,12 +52,10 @@ describe('Feature – Models – Insert', () => { it('returns inserted records via static method', async () => { createStore([{ model: User }]) - const data = await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + const data = await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) expect(data.users.length).toBe(2) expect(data.users[0]).toBeInstanceOf(User) @@ -74,13 +66,9 @@ describe('Feature – Models – Insert', () => { const user = new User() - await user.$insert({ - data: { id: 1, name: 'John Doe' } - }) + await user.$insert({ id: 1, name: 'John Doe' }) - await user.$insert({ - data: { id: 2, name: 'Jane Doe' } - }) + await user.$insert({ id: 2, name: 'Jane Doe' }) const expected = createState({ users: { @@ -92,17 +80,15 @@ describe('Feature – Models – Insert', () => { expect(store.state.entities).toEqual(expected) }) - it('can insert list of record via instance method', async () => { + it('can insert a list of records via instance method', async () => { const store = createStore([{ model: User }]) const user = new User() - await user.$insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await user.$insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const expected = createState({ users: { @@ -119,12 +105,10 @@ describe('Feature – Models – Insert', () => { const user = new User() - const data = await user.$insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + const data = await user.$insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) expect(data.users.length).toBe(2) expect(data.users[0]).toBeInstanceOf(User) @@ -214,9 +198,7 @@ describe('Feature – Models – Insert', () => { expect(data).toEqual(dataBefore) - await Author.insert({ - data - }) + await Author.insert(data) const expected = createState({ authors: { diff --git a/test/feature/models/InsertOrUpdate.spec.ts b/test/feature/models/InsertOrUpdate.spec.ts index bc699369..0af6b498 100644 --- a/test/feature/models/InsertOrUpdate.spec.ts +++ b/test/feature/models/InsertOrUpdate.spec.ts @@ -17,18 +17,14 @@ describe('Feature – Models – Insert Or Update', () => { it('can insert or update records via static method', async () => { const store = createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' } + ]) - await User.insertOrUpdate({ - data: [ - { id: 1, name: 'Jane Doe' }, - { id: 2, name: 'Johnny Doe' } - ] - }) + await User.insertOrUpdate([ + { id: 1, name: 'Jane Doe' }, + { id: 2, name: 'Johnny Doe' } + ]) const expected = createState({ users: { @@ -45,18 +41,14 @@ describe('Feature – Models – Insert Or Update', () => { const user = new User() - await User.insert({ - data: [ - { id: 1, name: 'John Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' } + ]) - await user.$insertOrUpdate({ - data: [ - { id: 1, name: 'Jane Doe' }, - { id: 2, name: 'Johnny Doe' } - ] - }) + await user.$insertOrUpdate([ + { id: 1, name: 'Jane Doe' }, + { id: 2, name: 'Johnny Doe' } + ]) const expected = createState({ users: { diff --git a/test/feature/models/Query.spec.ts b/test/feature/models/Query.spec.ts index 575dd3a5..a758d5f3 100644 --- a/test/feature/models/Query.spec.ts +++ b/test/feature/models/Query.spec.ts @@ -16,12 +16,10 @@ describe('Feature – Models – Query', () => { it('can begin query chain via static method', async () => { createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const users = User.query().get() @@ -37,12 +35,10 @@ describe('Feature – Models – Query', () => { it('can begin query chain via instance method', async () => { createStore([{ model: User }]) - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) + await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) const u = new User() diff --git a/test/feature/models/Update.spec.ts b/test/feature/models/Update.spec.ts index 123f83e5..bb6ebab0 100644 --- a/test/feature/models/Update.spec.ts +++ b/test/feature/models/Update.spec.ts @@ -178,13 +178,11 @@ describe('Feature – Models – Update', () => { const store = createStore([{ model: Role }]) - await Role.insert({ - data: { key_1: 1, key_2: 2, name: 'John Doe' } - }) + await Role.insert({ key_1: 1, key_2: 2, name: 'John Doe' }) - const user = Role.find([1, 2]) + const user = Role.find([1, 2]) as Role - await user?.$update({ name: 'Jane Doe' }) + await user.$update({ name: 'Jane Doe' }) const expected = createState({ roles: { @@ -231,10 +229,7 @@ describe('Feature – Models – Update', () => { data: { id: 1, name: 'John Doe' } }) - await user.$update({ - where: 1, - data: { name: 'Jane Doe' } - }) + await user.$update({ name: 'Jane Doe' }, { where: 1 }) const expected = createState({ users: { diff --git a/test/feature/relations/BelongsToMany_Retrieve.spec.ts b/test/feature/relations/BelongsToMany_Retrieve.spec.ts index 691c6ddb..bb6ddda9 100644 --- a/test/feature/relations/BelongsToMany_Retrieve.spec.ts +++ b/test/feature/relations/BelongsToMany_Retrieve.spec.ts @@ -66,20 +66,14 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { it('can resolve belongs to many relation', async () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: [{ id: 1 }, { id: 2 }] - }) + await User.create([{ id: 1 }, { id: 2 }]) - await Role.create({ - data: [{ id: 2 }, { id: 3 }] - }) + await Role.create([{ id: 2 }, { id: 3 }]) - await RoleUser.create({ - data: [ - { user_id: 1, role_id: 2 }, - { user_id: 1, role_id: 3 } - ] - }) + await RoleUser.create([ + { user_id: 1, role_id: 2 }, + { user_id: 1, role_id: 3 } + ]) const user = User.query().with('roles').find(1) as User @@ -94,9 +88,16 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { it('can resolve belongs to many relation with pivot data', async () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: [{ id: 1, roles: [{ id: 2, pivot: { level: 1 } }, { id: 3, pivot: { level: 2 } }] }, { id: 2 }] - }) + await User.create([ + { + id: 1, + roles: [ + { id: 2, pivot: { level: 1 } }, + { id: 3, pivot: { level: 2 } } + ] + }, + { id: 2 } + ]) const user = User.query().with('roles').find(1) as User @@ -113,20 +114,14 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { it('can resolve inverse belongs to many relation', async () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Role.create({ - data: [{ id: 2 }, { id: 3 }] - }) + await Role.create([{ id: 2 }, { id: 3 }]) - await RoleUser.create({ - data: [ - { user_id: 1, role_id: 2 }, - { user_id: 1, role_id: 3 } - ] - }) + await RoleUser.create([ + { user_id: 1, role_id: 2 }, + { user_id: 1, role_id: 3 } + ]) const role = Role.query().with('users').find(2) as Role @@ -137,9 +132,7 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { it('returns empty collection if the relation is not found', async () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) const user = User.query().with('roles').first() as User @@ -149,13 +142,9 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { it('returns empty collection if the Role relation from RoleUser is not found', async () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await RoleUser.create({ - data: [{ user_id: 1 }] - }) + await RoleUser.create([{ user_id: 1 }]) const user = User.query().with('roles').first() as User @@ -165,21 +154,15 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { it('can resolve belongs to many relation with has constraint', async () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Role.create({ - data: [{ id: 2 }, { id: 3 }] - }) + await Role.create([{ id: 2 }, { id: 3 }]) - await RoleUser.create({ - data: [ - { user_id: 1, role_id: 2 }, - { user_id: 1, role_id: 3 }, - { user_id: 2, role_id: 4 } - ] - }) + await RoleUser.create([ + { user_id: 1, role_id: 2 }, + { user_id: 1, role_id: 3 }, + { user_id: 2, role_id: 4 } + ]) const roles = Role.query().has('users').get() @@ -313,7 +296,7 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { } ] - await User.create({ data }) + await User.create(data) const users = User.query().with('roles.permissions').get() as User[] @@ -390,17 +373,11 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - await User.create({ - data: [{ id: 1 }] - }) + await User.create([{ id: 1 }]) - await Role.create({ - data: { id: 1 } - }) + await Role.create({ id: 1 }) - await RoleUser.create({ - data: { id: 1, user_id: 1, role_id: 1 } - }) + await RoleUser.create({ id: 1, user_id: 1, role_id: 1 }) const users = User.query().with('roles').get() @@ -413,10 +390,8 @@ describe('Feature – Relations – Belongs To Many – Retrieve', () => { createStore([{ model: User }, { model: Role }, { model: RoleUser }]) await User.create({ - data: { - id: 1, - roles: [{ id: 1 }, { id: 3 }, { id: 2 }] - } + id: 1, + roles: [{ id: 1 }, { id: 3 }, { id: 2 }] }) const ascending = User.query() diff --git a/test/feature/relations/HasMany.spec.ts b/test/feature/relations/HasMany.spec.ts index c01f15b2..4d33851f 100644 --- a/test/feature/relations/HasMany.spec.ts +++ b/test/feature/relations/HasMany.spec.ts @@ -1,4 +1,4 @@ -import { createStore, createState } from 'test/support/Helpers' +import { createStore } from 'test/support/Helpers' import Model from '@/model/Model' describe('Features – Relations – Has Many', () => { @@ -25,80 +25,78 @@ describe('Features – Relations – Has Many', () => { } it('can create data containing the has many relation', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: { - id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - } + await User.create({ + id: 1, + posts: [ + { id: 1 }, + { id: 2 } + ] }) - const expected = createState({ - users: { - 1: { $id: '1', id: 1, posts: [] } - }, - posts: { - 1: { $id: '1', id: 1, user_id: 1 }, - 2: { $id: '2', id: 2, user_id: 1 } - } - }) + const expectedUsers = [ + { $id: '1', id: 1, posts: [] } + ] + + expect(User.all()).toEqual(expectedUsers) + + const expectedPosts = [ + { $id: '1', id: 1, user_id: 1 }, + { $id: '2', id: 2, user_id: 1 } + ] - expect(store.state.entities).toEqual(expected) + expect(Post.all()).toEqual(expectedPosts) }) it('can update data and insert has many relation', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: { id: 1 } - }) + await User.create({ id: 1 }) - await store.dispatch('entities/users/update', { - insert: ['posts'], - data: { + await User.update( + { id: 1, posts: [ { id: 1 }, { id: 2 } ] - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, posts: [] } }, - posts: { - 1: { $id: '1', id: 1, user_id: 1 }, - 2: { $id: '2', id: 2, user_id: 1 } + { + insert: ['posts'] } - }) + ) - expect(store.state.entities).toEqual(expected) + const expectedUsers = [ + { $id: '1', id: 1, posts: [] } + ] + + expect(User.all()).toEqual(expectedUsers) + + const expectedPosts = [ + { $id: '1', id: 1, user_id: 1 }, + { $id: '2', id: 2, user_id: 1 } + ] + + expect(Post.all()).toEqual(expectedPosts) }) it('can resolve the has many relation', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/users/create', { - data: [ - { - id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - }, - { - id: 2, - posts: null - } - ] - }) + createStore([{ model: User }, { model: Post }]) + + await User.create([ + { + id: 1, + posts: [ + { id: 1 }, + { id: 2 } + ] + }, + { + id: 2, + posts: null + } + ]) const expected = [ { @@ -116,7 +114,7 @@ describe('Features – Relations – Has Many', () => { } ] - const user = store.getters['entities/users/query']().with('posts').all() + const user = User.query().with('posts').all() expect(user).toEqual(expected) }) @@ -135,16 +133,14 @@ describe('Features – Relations – Has Many', () => { } } - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: { - user_id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - } + await User.create({ + user_id: 1, + posts: [ + { id: 1 }, + { id: 2 } + ] }) const expected = { @@ -156,7 +152,7 @@ describe('Features – Relations – Has Many', () => { ] } - const user = store.getters['entities/users/query']().with('posts').find(1) + const user = User.query().with('posts').find(1) expect(user).toEqual(expected) }) @@ -185,27 +181,24 @@ describe('Features – Relations – Has Many', () => { } } - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) await User.create({ - data: { - id: 1, - local_key: 'local_key', - posts: [ - { id: 1 } - ] - } + id: 1, + local_key: 'local_key', + posts: [{ id: 1 }] }) - const expected = createState({ - users: { - 1: { $id: '1', id: 1, local_key: 'local_key', posts: [] } - }, - posts: { - 1: { $id: '1', id: 1, user_id: 'local_key' } - } - }) + const expectedUsers = [ + { $id: '1', id: 1, local_key: 'local_key', posts: [] } + ] + + expect(User.all()).toEqual(expectedUsers) + + const expectedPosts = [ + { $id: '1', id: 1, user_id: 'local_key' } + ] - expect(store.state.entities).toEqual(expected) + expect(Post.all()).toEqual(expectedPosts) }) }) diff --git a/test/feature/relations/HasManyBy.spec.ts b/test/feature/relations/HasManyBy.spec.ts index 99ab8620..c31c35b0 100644 --- a/test/feature/relations/HasManyBy.spec.ts +++ b/test/feature/relations/HasManyBy.spec.ts @@ -29,13 +29,11 @@ describe('Features – Relations – Has Many By', () => { const store = createStore([{ model: User }, { model: Post }]) await User.insert({ - data: { - id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - } + id: 1, + posts: [ + { id: 1 }, + { id: 2 } + ] }) const expected = createState({ @@ -76,9 +74,7 @@ describe('Features – Relations – Has Many By', () => { const store = createStore([{ model: User }, { model: Post }]) - await User.insert({ - data: { id: 1, posts: [] } - }) + await User.insert({ id: 1, posts: [] }) const expected = createState({ users: { @@ -115,9 +111,7 @@ describe('Features – Relations – Has Many By', () => { const store = createStore([{ model: User }, { model: Post }]) - await User.insert({ - data: { id: 1, posts: null } - }) + await User.insert({ id: 1, posts: null }) const expected = createState({ users: { @@ -155,13 +149,11 @@ describe('Features – Relations – Has Many By', () => { createStore([{ model: User }, { model: Post }]) await User.insert({ - data: { - id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - } + id: 1, + posts: [ + { id: 1 }, + { id: 2 } + ] }) const user = User.query().with('posts').find(1) @@ -204,12 +196,10 @@ describe('Features – Relations – Has Many By', () => { createStore([{ model: User }, { model: Post }]) - await User.insert({ - data: [ - { id: 1, posts: [{ id: 1 }, { id: 2 }] }, - { id: 2, post_ids: [], posts: [] } - ] - }) + await User.insert([ + { id: 1, posts: [{ id: 1 }, { id: 2 }] }, + { id: 2, post_ids: [], posts: [] } + ]) const users = User.query().with('posts').get() @@ -271,9 +261,7 @@ describe('Features – Relations – Has Many By', () => { createStore([{ model: User }, { model: Post }]) - await User.insert({ - data: { id: 1, posts: [{ id: 1 }, { id: 3 }, { id: 2 }] } - }) + await User.insert({ id: 1, posts: [{ id: 1 }, { id: 3 }, { id: 2 }] }) const ascending = User.query() .with('posts', query => { query.orderBy('id', 'asc') }) diff --git a/test/feature/relations/HasManyThrough.spec.ts b/test/feature/relations/HasManyThrough.spec.ts index 0042fec0..ca857129 100644 --- a/test/feature/relations/HasManyThrough.spec.ts +++ b/test/feature/relations/HasManyThrough.spec.ts @@ -58,13 +58,11 @@ describe('Features – Relations – Has Many Through', () => { const store = createStore([{ model: Country }, { model: User }, { model: Post }]) await Country.insert({ - data: { - id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - } + id: 1, + posts: [ + { id: 1 }, + { id: 2 } + ] }) const expected = createState({ @@ -135,29 +133,23 @@ describe('Features – Relations – Has Many Through', () => { createStore([{ model: Country }, { model: User }, { model: Post }]) - await User.insert({ - data: [ - { id: 1, country_id: 1 }, - { id: 2, country_id: 1 }, - { id: 3, country_id: 2 } - ] - }) + await User.insert([ + { id: 1, country_id: 1 }, + { id: 2, country_id: 1 }, + { id: 3, country_id: 2 } + ]) - await Post.insert({ - data: [ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 3 }, - { id: 4, user_id: 1 } - ] - }) + await Post.insert([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 3 }, + { id: 4, user_id: 1 } + ]) - await Country.insert({ - data: [ - { id: 1 }, - { id: 2 } - ] - }) + await Country.insert([ + { id: 1 }, + { id: 2 } + ]) const country = Country.query().with('posts').find(1) as Country @@ -230,29 +222,23 @@ describe('Features – Relations – Has Many Through', () => { createStore([{ model: Country }, { model: User }, { model: Post }]) - await User.insert({ - data: [ - { id: 'string-id-1', country_id: 'string-id-1' }, - { id: 'string-id-2', country_id: 'string-id-1' }, - { id: 'string-id-3', country_id: 'string-id-2' } - ] - }) + await User.insert([ + { id: 'string-id-1', country_id: 'string-id-1' }, + { id: 'string-id-2', country_id: 'string-id-1' }, + { id: 'string-id-3', country_id: 'string-id-2' } + ]) - await Post.insert({ - data: [ - { id: 'string-id-1', user_id: 'string-id-1' }, - { id: 'string-id-2', user_id: 'string-id-2' }, - { id: 'string-id-3', user_id: 'string-id-3' }, - { id: 'string-id-4', user_id: 'string-id-1' } - ] - }) + await Post.insert([ + { id: 'string-id-1', user_id: 'string-id-1' }, + { id: 'string-id-2', user_id: 'string-id-2' }, + { id: 'string-id-3', user_id: 'string-id-3' }, + { id: 'string-id-4', user_id: 'string-id-1' } + ]) - await Country.insert({ - data: [ - { id: 'string-id-1' }, - { id: 'string-id-2' } - ] - }) + await Country.insert([ + { id: 'string-id-1' }, + { id: 'string-id-2' } + ]) const country = Country.query().with('posts').find('string-id-1') as Country @@ -322,9 +308,7 @@ describe('Features – Relations – Has Many Through', () => { createStore([{ model: Country }, { model: User }, { model: Post }]) - await Country.insert({ - data: [{ id: 1 }] - }) + await Country.insert([{ id: 1 }]) const country = Country.query().with('posts').find(1) as Country @@ -386,15 +370,11 @@ describe('Features – Relations – Has Many Through', () => { createStore([{ model: Country }, { model: User }, { model: Post }]) await User.insert({ - data: { - id: 1, - country_id: 1 - } + id: 1, + country_id: 1 }) - await Country.insert({ - data: [{ id: 1 }] - }) + await Country.insert([{ id: 1 }]) const country = Country.query().with('posts').find(1) as Country @@ -461,28 +441,22 @@ describe('Features – Relations – Has Many Through', () => { createStore([{ model: Country }, { model: User }, { model: Post }]) - await User.insert({ - data: [ - { u_id: 1, country_id: 1 }, - { u_id: 2, country_id: 1 }, - { u_id: 3, country_id: 2 } - ] - }) + await User.insert([ + { u_id: 1, country_id: 1 }, + { u_id: 2, country_id: 1 }, + { u_id: 3, country_id: 2 } + ]) - await Post.insert({ - data: [ - { p_id: 1, user_id: 1 }, - { p_id: 2, user_id: 2 }, - { p_id: 3, user_id: 3 } - ] - }) + await Post.insert([ + { p_id: 1, user_id: 1 }, + { p_id: 2, user_id: 2 }, + { p_id: 3, user_id: 3 } + ]) - await Country.insert({ - data: [ - { c_id: 1 }, - { c_id: 2 } - ] - }) + await Country.insert([ + { c_id: 1 }, + { c_id: 2 } + ]) const country = Country.query().with('posts').find(1) as Country @@ -559,28 +533,22 @@ describe('Features – Relations – Has Many Through', () => { createStore([{ model: Country }, { model: User }, { model: Post }]) - await User.insert({ - data: [ - { id: 11, u_id: 1, country_id: 1 }, - { id: 12, u_id: 2, country_id: 1 }, - { id: 13, u_id: 3, country_id: 2 } - ] - }) - - await Post.insert({ - data: [ - { id: 11, p_id: 1, user_id: 1 }, - { id: 12, p_id: 2, user_id: 2 }, - { id: 13, p_id: 3, user_id: 3 } - ] - }) - - await Country.insert({ - data: [ - { id: 11, c_id: 1 }, - { id: 12, c_id: 2 } - ] - }) + await User.insert([ + { id: 11, u_id: 1, country_id: 1 }, + { id: 12, u_id: 2, country_id: 1 }, + { id: 13, u_id: 3, country_id: 2 } + ]) + + await Post.insert([ + { id: 11, p_id: 1, user_id: 1 }, + { id: 12, p_id: 2, user_id: 2 }, + { id: 13, p_id: 3, user_id: 3 } + ]) + + await Country.insert([ + { id: 11, c_id: 1 }, + { id: 12, c_id: 2 } + ]) const country = Country.query().with('posts').find(11) as Country diff --git a/test/feature/relations/HasOne.spec.ts b/test/feature/relations/HasOne.spec.ts index fec05151..c28e2c62 100644 --- a/test/feature/relations/HasOne.spec.ts +++ b/test/feature/relations/HasOne.spec.ts @@ -28,10 +28,8 @@ describe('Features – Relations – Has One', () => { const store = createStore([{ model: User }, { model: Phone }]) await User.insert({ - data: { - id: 1, - phone: { id: 1, user_id: 1 } - } + id: 1, + phone: { id: 1, user_id: 1 } }) const expected = createState({ @@ -49,9 +47,7 @@ describe('Features – Relations – Has One', () => { it('can create data when the has one relation is empty', async () => { const store = createStore([{ model: User }, { model: Phone }]) - await User.insert({ - data: { $id: '1', id: 1 } - }) + await User.insert({ $id: '1', id: 1 }) const expected = createState({ users: { @@ -67,10 +63,8 @@ describe('Features – Relations – Has One', () => { const store = createStore([{ model: User }, { model: Phone }]) await User.insert({ - data: { - id: 1, - phone: null - } + id: 1, + phone: null }) const expected = createState({ @@ -86,9 +80,7 @@ describe('Features – Relations – Has One', () => { it('can create data when the has one relation is set to primitive value', async () => { const store = createStore([{ model: User }, { model: Phone }]) - await User.insert({ - data: { id: 1, phone: 1 } - }) + await User.insert({ id: 1, phone: 1 }) const expected = createState({ users: { @@ -103,18 +95,16 @@ describe('Features – Relations – Has One', () => { it('it can create data containing mixed fields value', async () => { const store = createStore([{ model: User }, { model: Phone }]) - await User.insert({ - data: [ - { - id: 1, - phone: { id: 1, user_id: 1 } - }, - { - id: 2, - phone: null - } - ] - }) + await User.insert([ + { + id: 1, + phone: { id: 1, user_id: 1 } + }, + { + id: 2, + phone: null + } + ]) const expected = createState({ users: { @@ -145,10 +135,8 @@ describe('Features – Relations – Has One', () => { const store = createStore([{ model: User }, { model: Phone }]) await User.insert({ - data: { - id: 1, - phone: { id: 1 } - } + id: 1, + phone: { id: 1 } }) const expected = createState({ @@ -167,10 +155,8 @@ describe('Features – Relations – Has One', () => { createStore([{ model: User }, { model: Phone }]) await User.insert({ - data: { - id: 1, - phone: { id: 1 } - } + id: 1, + phone: { id: 1 } }) const expected = { @@ -192,9 +178,7 @@ describe('Features – Relations – Has One', () => { it('can resolve the empty has one relation', async () => { const store = createStore([{ model: User }, { model: Phone }]) - await User.insert({ - data: { id: 1 } - }) + await User.insert({ id: 1 }) const expected = { $id: '1', id: 1, phone: null } diff --git a/test/feature/relations/MorphMany_Persist.spec.ts b/test/feature/relations/MorphMany_Persist.spec.ts index e5b0358c..5d5680b2 100644 --- a/test/feature/relations/MorphMany_Persist.spec.ts +++ b/test/feature/relations/MorphMany_Persist.spec.ts @@ -41,13 +41,11 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) await Post.create({ - data: { - id: 1, - comments: [ - { id: 2, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { id: 3, body: 'comment2', commentable_id: 1, commentable_type: 'posts' } - ] - } + id: 1, + comments: [ + { id: 2, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { id: 3, body: 'comment2', commentable_id: 1, commentable_type: 'posts' } + ] }) createState({ @@ -106,23 +104,21 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [ - { - id: 1, - comments: [ - { id: 2, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { id: 3, body: 'comment2', commentable_id: 1, commentable_type: 'posts' } - ] - }, - { - id: 2, - comments: [ - { id: 4, body: 'comment3', commentable_id: 2, commentable_type: 'posts' } - ] - } - ] - }) + await Post.create([ + { + id: 1, + comments: [ + { id: 2, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { id: 3, body: 'comment2', commentable_id: 1, commentable_type: 'posts' } + ] + }, + { + id: 2, + comments: [ + { id: 4, body: 'comment3', commentable_id: 2, commentable_type: 'posts' } + ] + } + ]) expect(store.state.entities.posts.data['1'].id).toBe(1) expect(store.state.entities.posts.data['2'].id).toBe(2) @@ -173,13 +169,11 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) await Post.create({ - data: { - id: 1, - comments: [ - { id: 2, body: 'comment1' }, - { id: 3, body: 'comment2' } - ] - } + id: 1, + comments: [ + { id: 2, body: 'comment1' }, + { id: 3, body: 'comment2' } + ] }) createState({ @@ -239,13 +233,11 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) await Post.create({ - data: { - id: 1, - comments: [ - { id: 2, body: 'comment1', commentable_id: 1 }, - { id: 3, body: 'comment2', commentable_id: 2 } - ] - } + id: 1, + comments: [ + { id: 2, body: 'comment1', commentable_id: 1 }, + { id: 3, body: 'comment2', commentable_id: 2 } + ] }) expect(store.state.entities.posts.data['1'].id).toBe(1) @@ -293,12 +285,10 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Comment.create({ - data: [ - { id: 2, body: 'comment1' }, - { id: 3, body: 'comment2' } - ] - }) + await Comment.create([ + { id: 2, body: 'comment1' }, + { id: 3, body: 'comment2' } + ]) createState({ posts: {}, @@ -368,17 +358,15 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: User }, { model: Post }, { model: Video }, { model: Comment }]) await User.create({ - data: { - id: 1, - posts: [{ - id: 5, - user_id: 1, - comments: [ - { id: 2, body: 'comment1', commentable_id: 1 }, - { id: 3, body: 'comment2', commentable_id: 2 } - ] - }] - } + id: 1, + posts: [{ + id: 5, + user_id: 1, + comments: [ + { id: 2, body: 'comment1', commentable_id: 1 }, + { id: 3, body: 'comment2', commentable_id: 2 } + ] + }] }) createState({ @@ -432,9 +420,7 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { id: 1, comments: null } - }) + await Post.create({ id: 1, comments: null }) const expected = createState({ posts: { @@ -484,14 +470,12 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { - id: 1, - comments: [ - { id: 1, body: 'comment1', commentabe_id: 1, commentable_type: 'posts' }, - { id: 2, body: 'comment2', commentabe_id: 1, commentable_type: 'posts' } - ] - } + await Post.create({ + id: 1, + comments: [ + { id: 1, body: 'comment1', commentabe_id: 1, commentable_type: 'posts' }, + { id: 2, body: 'comment2', commentabe_id: 1, commentable_type: 'posts' } + ] }) const post = store.getters['entities/posts/query']().with('comments').find(1) @@ -502,7 +486,7 @@ describe('Feature – Relations – Morph Many – Persist', () => { expect(post.comments[1]).toBeInstanceOf(Comment) }) - it('generates id or type for the moprh relation', async () => { + it('generates id or type for the morph relation', async () => { class Post extends Model { static entity = 'posts' @@ -540,14 +524,12 @@ describe('Feature – Relations – Morph Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { - id: 1, - comments: [ - { id: 1, body: 'comment1' }, - { id: 2, body: 'comment2' } - ] - } + await Post.create({ + id: 1, + comments: [ + { id: 1, body: 'comment1' }, + { id: 2, body: 'comment2' } + ] }) const post = store.getters['entities/posts/query']().with('comments').find(1) diff --git a/test/feature/relations/MorphMany_Retrieve.spec.ts b/test/feature/relations/MorphMany_Retrieve.spec.ts index 021c9f97..5cf58eb7 100644 --- a/test/feature/relations/MorphMany_Retrieve.spec.ts +++ b/test/feature/relations/MorphMany_Retrieve.spec.ts @@ -65,22 +65,16 @@ describe('Feature – Relations – Morph Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ id: 1 }, { id: 5 }, { id: 6 }] - }) - - await Video.create({ - data: { id: 3 } - }) - - await Comment.create({ - data: [ - { id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, - { id: '3', body: 'comment3', commentable_id: 1, commentable_type: 'posts' }, - { id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } - ] - }) + await Post.create([{ id: 1 }, { id: 5 }, { id: 6 }]) + + await Video.create({ id: 3 }) + + await Comment.create([ + { id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, + { id: '3', body: 'comment3', commentable_id: 1, commentable_type: 'posts' }, + { id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } + ]) const post = Post.query().with('comments').find(1) as Post @@ -157,22 +151,16 @@ describe('Feature – Relations – Morph Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ post_id: 1 }, { post_id: 5 }] - }) + await Post.create([{ post_id: 1 }, { post_id: 5 }]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) - await Comment.create({ - data: [ - { id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, - { id: '3', body: 'comment3', commentable_id: 1, commentable_type: 'posts' }, - { id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } - ] - }) + await Comment.create([ + { id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, + { id: '3', body: 'comment3', commentable_id: 1, commentable_type: 'posts' }, + { id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } + ]) const post = Post.query().with('comments').find(1) as Post @@ -247,22 +235,16 @@ describe('Feature – Relations – Morph Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ id: 2, post_id: 1 }, { id: 3, post_id: 5 }] - }) + await Post.create([{ id: 2, post_id: 1 }, { id: 3, post_id: 5 }]) - await Video.create({ - data: [{ id: 3 }, { id: 4 }] - }) + await Video.create([{ id: 3 }, { id: 4 }]) - await Comment.create({ - data: [ - { id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, - { id: '3', body: 'comment3', commentable_id: 1, commentable_type: 'posts' }, - { id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } - ] - }) + await Comment.create([ + { id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, + { id: '3', body: 'comment3', commentable_id: 1, commentable_type: 'posts' }, + { id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } + ]) const post = Post.query().with('comments').find(2) as Post @@ -333,13 +315,9 @@ describe('Feature – Relations – Morph Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.insert({ - data: { id: 1, comments: [{ id: 1 }, { id: 3 }, { id: 2 }] } - }) + await Post.insert({ id: 1, comments: [{ id: 1 }, { id: 3 }, { id: 2 }] }) - await Video.insert({ - data: { id: 1, comments: [{ id: 4 }, { id: 6 }, { id: 5 }] } - }) + await Video.insert({ id: 1, comments: [{ id: 4 }, { id: 6 }, { id: 5 }] }) const post = Post.query().with('comments', query => { query.orderBy('id', 'asc') }).find(1) as Post diff --git a/test/feature/relations/MorphOne_Persist.spec.ts b/test/feature/relations/MorphOne_Persist.spec.ts index 9aa13dab..a8848a47 100644 --- a/test/feature/relations/MorphOne_Persist.spec.ts +++ b/test/feature/relations/MorphOne_Persist.spec.ts @@ -30,15 +30,13 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { + await Post.create({ + id: 1, + comment: { id: 1, - comment: { - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1 - } + body: 'The Body', + commentable_type: 'posts', + commentable_id: 1 } }) @@ -93,10 +91,8 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) await Post.create({ - data: { - id: 1, - comment: { id: 2, body: 'comment1' } - } + id: 1, + comment: { id: 2, body: 'comment1' } }) expect(store.state.entities.posts.data['1'].id).toBe(1) @@ -144,10 +140,8 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) await Post.create({ - data: { - id: 1, - comment: { id: 2, body: 'comment1', commentable_id: 1 } - } + id: 1, + comment: { id: 2, body: 'comment1', commentable_id: 1 } }) createState({ @@ -204,12 +198,10 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Comment.create({ - data: [ - { id: 2, body: 'comment1' }, - { id: 3, body: 'comment2' } - ] - }) + await Comment.create([ + { id: 2, body: 'comment1' }, + { id: 3, body: 'comment2' } + ]) expect(store.state.entities.comments.data['2'].id).toBe(2) expect(store.state.entities.comments.data['2'].commentable_id).toBe(null) @@ -270,14 +262,12 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: User }, { model: Post }, { model: Video }, { model: Comment }]) await User.create({ - data: { - id: 1, - posts: [{ - id: 5, - user_id: 1, - comment: { id: 2, body: 'comment1', commentable_id: 1 } - }] - } + id: 1, + posts: [{ + id: 5, + user_id: 1, + comment: { id: 2, body: 'comment1', commentable_id: 1 } + }] }) createState({ @@ -328,23 +318,21 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: [ - { + await Post.create([ + { + id: 1, + comment: { id: 1, - comment: { - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1 - } - }, - { - id: 2, - comment: null + body: 'The Body', + commentable_type: 'posts', + commentable_id: 1 } - ] - }) + }, + { + id: 2, + comment: null + } + ]) const expected = createState({ posts: { @@ -397,11 +385,9 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { - id: 1, - comment: { id: 1, body: 'comment1', commentabe_id: 1, commentable_type: 'posts' } - } + await Post.create({ + id: 1, + comment: { id: 1, body: 'comment1', commentabe_id: 1, commentable_type: 'posts' } }) const post = store.getters['entities/posts/query']().with('comment').find(1) @@ -410,7 +396,7 @@ describe('Features – Relations – Morph One – Persist', () => { expect(post.comment).toBeInstanceOf(Comment) }) - it('generates id or type for the moprh relation', async () => { + it('generates id or type for the morph relation', async () => { class Post extends Model { static entity = 'posts' @@ -448,11 +434,9 @@ describe('Features – Relations – Morph One – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { - id: 1, - comment: { id: 1, body: 'comment1' } - } + await Post.create({ + id: 1, + comment: { id: 1, body: 'comment1' } }) const post = store.getters['entities/posts/query']().with('comment').find(1) diff --git a/test/feature/relations/MorphOne_Retrieve.spec.ts b/test/feature/relations/MorphOne_Retrieve.spec.ts index c447f5d6..72718775 100644 --- a/test/feature/relations/MorphOne_Retrieve.spec.ts +++ b/test/feature/relations/MorphOne_Retrieve.spec.ts @@ -65,22 +65,16 @@ describe('Feature – Relations – Morph One – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ id: 1 }, { id: 5 }] - }) - - await Video.create({ - data: { id: 3 } - }) - - await Comment.create({ - data: [ - { $id: '1', id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { $id: '2', id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, - { $id: '3', id: '3', body: 'comment3', commentable_id: 2, commentable_type: 'posts' }, - { $id: '4', id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } - ] - }) + await Post.create([{ id: 1 }, { id: 5 }]) + + await Video.create({ id: 3 }) + + await Comment.create([ + { $id: '1', id: 1, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { $id: '2', id: 2, body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, + { $id: '3', id: 3, body: 'comment3', commentable_id: 2, commentable_type: 'posts' }, + { $id: '4', id: 4, body: 'comment4', commentable_id: 5, commentable_type: 'posts' } + ]) const post = Post.query().with('comment').find(1) as Post @@ -149,13 +143,9 @@ describe('Feature – Relations – Morph One – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ id: 1 }, { id: 5 }] - }) + await Post.create([{ id: 1 }, { id: 5 }]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) const post = Post.query().with('comments').find(1) as Post @@ -226,22 +216,16 @@ describe('Feature – Relations – Morph One – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ post_id: 1 }, { post_id: 5 }] - }) + await Post.create([{ post_id: 1 }, { post_id: 5 }]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) - await Comment.create({ - data: [ - { $id: '1', id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { $id: '2', id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, - { $id: '3', id: '3', body: 'comment3', commentable_id: 2, commentable_type: 'posts' }, - { $id: '4', id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } - ] - }) + await Comment.create([ + { $id: '1', id: 1, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { $id: '2', id: 2, body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, + { $id: '3', id: 3, body: 'comment3', commentable_id: 2, commentable_type: 'posts' }, + { $id: '4', id: 4, body: 'comment4', commentable_id: 5, commentable_type: 'posts' } + ]) const post = Post.query().with('comment').find(1) as Post @@ -314,22 +298,16 @@ describe('Feature – Relations – Morph One – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await Post.create({ - data: [{ id: 2, post_id: 1 }, { id: 3, post_id: 5 }] - }) - - await Video.create({ - data: { id: 3 } - }) - - await Comment.create({ - data: [ - { $id: '1', id: '1', body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, - { $id: '2', id: '2', body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, - { $id: '3', id: '3', body: 'comment3', commentable_id: 2, commentable_type: 'posts' }, - { $id: '4', id: '4', body: 'comment4', commentable_id: 5, commentable_type: 'posts' } - ] - }) + await Post.create([{ id: 2, post_id: 1 }, { id: 3, post_id: 5 }]) + + await Video.create({ id: 3 }) + + await Comment.create([ + { $id: '1', id: 1, body: 'comment1', commentable_id: 1, commentable_type: 'posts' }, + { $id: '2', id: 2, body: 'comment2', commentable_id: 3, commentable_type: 'videos' }, + { $id: '3', id: 3, body: 'comment3', commentable_id: 2, commentable_type: 'posts' }, + { $id: '4', id: 4, body: 'comment4', commentable_id: 5, commentable_type: 'posts' } + ]) const post = Post.query().with('comment').find(2) as Post diff --git a/test/feature/relations/MorphTo.spec.ts b/test/feature/relations/MorphTo.spec.ts index acfca4a1..5f610421 100644 --- a/test/feature/relations/MorphTo.spec.ts +++ b/test/feature/relations/MorphTo.spec.ts @@ -1,3 +1,4 @@ +/* tslint:disable:variable-name */ import { createStore, createState } from 'test/support/Helpers' import Model from '@/model/Model' @@ -6,6 +7,12 @@ describe('Features – Relations – Morph To', () => { class Post extends Model { static entity = 'posts' + // @Attribute + id!: number + + // @MorphOne(Comment, 'commentable_id', 'commentable_type') + comment!: Comment + static fields () { return { id: this.attr(null), @@ -17,6 +24,21 @@ describe('Features – Relations – Morph To', () => { class Comment extends Model { static entity = 'comments' + // @Attribute + id!: number + + // @Attribute('') + body!: string + + // @Attribute + commentable_id!: number + + // @Attribute + commentable_type!: string + + // @MorphTo('commentable_id', 'commentable_type') + commentable!: Post + static fields () { return { id: this.attr(null), @@ -30,14 +52,12 @@ describe('Features – Relations – Morph To', () => { const store = createStore([{ model: Post }, { model: Comment }]) - await store.dispatch('entities/comments/create', { - data: { - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1, - commentable: { id: 1 } - } + await Comment.create({ + id: 1, + body: 'The Body', + commentable_type: 'posts', + commentable_id: 1, + commentable: { id: 1 } }) const expected = createState({ @@ -63,6 +83,12 @@ describe('Features – Relations – Morph To', () => { class Post extends Model { static entity = 'posts' + // @Attribute + id!: number + + // @MorphMany(Comment, 'commentable_id', 'commentable_type') + comments!: Comment[] + static fields () { return { id: this.attr(null), @@ -74,6 +100,12 @@ describe('Features – Relations – Morph To', () => { class Video extends Model { static entity = 'videos' + // @Attribute + id!: number + + // @MorphMany(Comment, 'commentable_id', 'commentable_type') + comments!: Comment[] + static fields () { return { id: this.attr(null), @@ -85,6 +117,21 @@ describe('Features – Relations – Morph To', () => { class Comment extends Model { static entity = 'comments' + // @Attribute + id!: number + + // @Attribute('') + body!: string + + // @Attribute + commentable_id!: number + + // @Attribute + commentable_type!: string + + // @MorphTo('commentable_id', 'commentable_type') + commentable!: Post | Video + static fields () { return { id: this.attr(null), @@ -96,33 +143,31 @@ describe('Features – Relations – Morph To', () => { } } - const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) + createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await store.dispatch('entities/posts/insert', { - data: { - id: 1, - comments: [ - { id: 1, body: 'comment1' }, - { id: 2, body: 'comment2' } - ] - } + await Post.insert({ + id: 1, + comments: [ + { id: 1, body: 'comment1' }, + { id: 2, body: 'comment2' } + ] }) - await store.dispatch('entities/videos/insert', { - data: { - id: 1, - comments: [ - { id: 3, body: 'comment3' }, - { id: 4, body: 'comment4' } - ] - } + await Video.insert({ + id: 1, + comments: [ + { id: 3, body: 'comment3' }, + { id: 4, body: 'comment4' } + ] }) - const comments = store.getters['entities/comments/query']().with('commentable').get() + const comments = Comment.query().with('commentable').get() expect(comments.length).toBe(4) + expect(comments[0]).toBeInstanceOf(Comment) expect(comments[0].commentable).toBeInstanceOf(Post) + expect(comments[2]).toBeInstanceOf(Comment) expect(comments[2].commentable).toBeInstanceOf(Video) }) @@ -131,6 +176,12 @@ describe('Features – Relations – Morph To', () => { class Post extends Model { static entity = 'posts' + // @Attribute + id!: number + + // @MorphMany(Comment, 'commentable_id', 'commentable_type') + comments!: Comment[] + static fields () { return { id: this.attr(null), @@ -142,6 +193,12 @@ describe('Features – Relations – Morph To', () => { class Video extends Model { static entity = 'videos' + // @Attribute + id!: number + + // @MorphMany(Comment, 'commentable_id', 'commentable_type') + comments!: Comment[] + static fields () { return { id: this.attr(null), @@ -153,6 +210,21 @@ describe('Features – Relations – Morph To', () => { class Comment extends Model { static entity = 'comments' + // @Attribute + id!: number + + // @Attribute('') + body!: string + + // @Attribute + commentable_id!: number + + // @Attribute + commentable_type!: string + + // @MorphTo('commentable_id', 'commentable_type') + commentable!: Post | Video + static fields () { return { id: this.attr(null), @@ -164,16 +236,14 @@ describe('Features – Relations – Morph To', () => { } } - const store = createStore([{ model: Post }, { model: Video }, { model: Comment }]) + createStore([{ model: Post }, { model: Video }, { model: Comment }]) - await store.dispatch('entities/comments/insert', { - data: { - id: 1, - body: 'Body 01' - } + await Comment.insert({ + id: 1, + body: 'Body 01' }) - const comment = store.getters['entities/comments/query']().with('commentable').find(1) + const comment = Comment.query().with('commentable').find(1) as Comment expect(comment.commentable).toBe(null) }) diff --git a/test/feature/relations/MorphToMany_Persist.spec.ts b/test/feature/relations/MorphToMany_Persist.spec.ts index e6bcc6c8..26238d4d 100644 --- a/test/feature/relations/MorphToMany_Persist.spec.ts +++ b/test/feature/relations/MorphToMany_Persist.spec.ts @@ -1,3 +1,4 @@ +/* tslint:disable:variable-name */ import { createStore, createState } from 'test/support/Helpers' import Model from '@/model/Model' @@ -52,13 +53,11 @@ describe('Features – Relations – Morph To Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Post.create({ - data: { - id: 1, - tags: [ - { id: 2, name: 'news', pivot: { public: true } }, - { id: 3, name: 'cast' } - ] - } + id: 1, + tags: [ + { id: 2, name: 'news', pivot: { public: true } }, + { id: 3, name: 'cast' } + ] }) expect(store.state.entities.posts.data['1'].id).toBe(1) @@ -121,9 +120,7 @@ describe('Features – Relations – Morph To Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await store.dispatch('entities/posts/create', { - data: { id: 1 } - }) + await Post.create({ id: 1 }) const expected = { 1: { $id: '1', id: 1, tags: [] } @@ -181,13 +178,11 @@ describe('Features – Relations – Morph To Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Post.create({ - data: { - id: 1, - tags: [ - { id: 2, name: 'news' }, - { id: 3, name: 'cast' } - ] - } + id: 1, + tags: [ + { id: 2, name: 'news' }, + { id: 3, name: 'cast' } + ] }) const expected = createState({ @@ -259,13 +254,11 @@ describe('Features – Relations – Morph To Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Post.create({ - data: { - title: 'Post title.', - tags: [ - { id: 2, name: 'news' }, - { id: 3, name: 'cast' } - ] - } + title: 'Post title.', + tags: [ + { id: 2, name: 'news' }, + { id: 3, name: 'cast' } + ] }) const expected = createState({ @@ -336,13 +329,11 @@ describe('Features – Relations – Morph To Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Post.create({ - data: { - id: 1, - tags: [ - { id: 2, name: 'news', tag_pivot: { public: true } }, - { id: 3, name: 'cast' } - ] - } + id: 1, + tags: [ + { id: 2, name: 'news', tag_pivot: { public: true } }, + { id: 3, name: 'cast' } + ] }) expect(store.state.entities.taggables.data['1_2_posts'].public).toBe(true) @@ -353,6 +344,12 @@ describe('Features – Relations – Morph To Many – Persist', () => { class Post extends Model { static entity = 'posts' + // @Attribute + id!: number + + // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') + tags!: Tag[] + static fields () { return { id: this.attr(null), @@ -364,6 +361,12 @@ describe('Features – Relations – Morph To Many – Persist', () => { class Video extends Model { static entity = 'videos' + // @Attribute + id!: number + + // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') + tags!: Tag[] + static fields () { return { id: this.attr(null), @@ -375,6 +378,12 @@ describe('Features – Relations – Morph To Many – Persist', () => { class Tag extends Model { static entity = 'tag' + // @Attribute + id!: number + + // @Attribute('') + name!: string + static fields () { return { id: this.attr(null), @@ -386,6 +395,18 @@ describe('Features – Relations – Morph To Many – Persist', () => { class Taggable extends Model { static entity = 'taggables' + // @Attribute + id!: number + + // @Attribute + tag_id!: number + + // @Attribute + taggable_id!: number + + // @Attribute + taggable_type!: string + static fields () { return { id: this.attr(null), @@ -396,28 +417,26 @@ describe('Features – Relations – Morph To Many – Persist', () => { } } - const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) + createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await store.dispatch('entities/posts/create', { - data: [ - { - id: 1, - tags: [ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ] - }, - { - id: 2, - tags: [ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ] - } - ] - }) + await Post.create([ + { + id: 1, + tags: [ + { id: 1, name: 'news' }, + { id: 2, name: 'cast' } + ] + }, + { + id: 2, + tags: [ + { id: 1, name: 'news' }, + { id: 2, name: 'cast' } + ] + } + ]) - const post = store.getters['entities/posts/query']().with('tags').find(1) + const post = Post.query().with('tags').find(1) as Post expect(post).toBeInstanceOf(Post) expect(post.tags.length).toBe(2) diff --git a/test/feature/relations/MorphToMany_Retrieve.spec.ts b/test/feature/relations/MorphToMany_Retrieve.spec.ts index 575e5eda..e1293feb 100644 --- a/test/feature/relations/MorphToMany_Retrieve.spec.ts +++ b/test/feature/relations/MorphToMany_Retrieve.spec.ts @@ -89,25 +89,17 @@ describe('Feature – Relations – Morph To Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [{ id: 1 }, { id: 5 }, { id: 6 }] - }) + await Post.create([{ id: 1 }, { id: 5 }, { id: 6 }]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) - await Tag.create({ - data: [{ id: 1, name: 'news' }, { id: 2, name: 'cast' }] - }) + await Tag.create([{ id: 1, name: 'news' }, { id: 2, name: 'cast' }]) - await Taggable.create({ - data: [ - { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts', is_public: true }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos', is_public: true }, - { id: 3, tag_id: 2, taggable_id: 1, taggable_type: 'posts', is_public: false } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts', is_public: true }, + { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos', is_public: true }, + { id: 3, tag_id: 2, taggable_id: 1, taggable_type: 'posts', is_public: false } + ]) const post = Post.query().with('tags').find(1) as Post @@ -203,25 +195,17 @@ describe('Feature – Relations – Morph To Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [{ post_id: 1 }, { post_id: 5 }] - }) + await Post.create([{ post_id: 1 }, { post_id: 5 }]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) - await Tag.create({ - data: [{ id: 1, name: 'news' }, { id: 2, name: 'cast' }] - }) + await Tag.create([{ id: 1, name: 'news' }, { id: 2, name: 'cast' }]) - await Taggable.create({ - data: [ - { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 2, taggable_id: 1, taggable_type: 'posts' } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts' }, + { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, + { id: 3, tag_id: 2, taggable_id: 1, taggable_type: 'posts' } + ]) const post = Post.query().with('tags').find(1) as Post @@ -313,28 +297,20 @@ describe('Feature – Relations – Morph To Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [ - { id: 1, post_id: 10 }, - { id: 5, post_id: 11 } - ] - }) + await Post.create([ + { id: 1, post_id: 10 }, + { id: 5, post_id: 11 } + ]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) - await Tag.create({ - data: [{ id: 1, name: 'news' }, { id: 2, name: 'cast' }] - }) + await Tag.create([{ id: 1, name: 'news' }, { id: 2, name: 'cast' }]) - await Taggable.create({ - data: [ - { id: 1, tag_id: 1, taggable_id: 10, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 2, taggable_id: 10, taggable_type: 'posts' } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 1, taggable_id: 10, taggable_type: 'posts' }, + { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, + { id: 3, tag_id: 2, taggable_id: 10, taggable_type: 'posts' } + ]) const post = Post.query().with('tags').find(1) as Post @@ -430,31 +406,23 @@ describe('Feature – Relations – Morph To Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [ - { id: 1, post_id: 10 }, - { id: 5, post_id: 11 } - ] - }) + await Post.create([ + { id: 1, post_id: 10 }, + { id: 5, post_id: 11 } + ]) - await Video.create({ - data: { id: 3 } - }) + await Video.create({ id: 3 }) - await Tag.create({ - data: [ - { id: 1, tag_id: 100, name: 'news' }, - { id: 2, tag_id: 101, name: 'cast' } - ] - }) + await Tag.create([ + { id: 1, tag_id: 100, name: 'news' }, + { id: 2, tag_id: 101, name: 'cast' } + ]) - await Taggable.create({ - data: [ - { id: 1, tag_id: 100, taggable_id: 10, taggable_type: 'posts' }, - { id: 2, tag_id: 100, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 101, taggable_id: 10, taggable_type: 'posts' } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 100, taggable_id: 10, taggable_type: 'posts' }, + { id: 2, tag_id: 100, taggable_id: 3, taggable_type: 'videos' }, + { id: 3, tag_id: 101, taggable_id: 10, taggable_type: 'posts' } + ]) const post = Post.query().with('tags').find(1) as Post @@ -539,17 +507,13 @@ describe('Feature – Relations – Morph To Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Post.insert({ - data: { - id: 1, - tags: [{ id: 1 }, { id: 3 }, { id: 2 }] - } + id: 1, + tags: [{ id: 1 }, { id: 3 }, { id: 2 }] }) await Video.insert({ - data: { - id: 1, - tags: [{ id: 1 }, { id: 3 }, { id: 2 }] - } + id: 1, + tags: [{ id: 1 }, { id: 3 }, { id: 2 }] }) const post = Post.query() diff --git a/test/feature/relations/MorphedByMany_Persist.spec.ts b/test/feature/relations/MorphedByMany_Persist.spec.ts index 1c9f1acc..18d888bf 100644 --- a/test/feature/relations/MorphedByMany_Persist.spec.ts +++ b/test/feature/relations/MorphedByMany_Persist.spec.ts @@ -53,12 +53,10 @@ describe('Feature – Relations – Morphed By Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Tag.create({ - data: { - id: 1, - name: 'news', - posts: [{ id: 1, pivot: { public: true } }, { id: 2 }], - videos: [{ id: 3, pivot: { public: true } }, { id: 4 }] - } + id: 1, + name: 'news', + posts: [{ id: 1, pivot: { public: true } }, { id: 2 }], + videos: [{ id: 3, pivot: { public: true } }, { id: 4 }] }) expect(store.state.entities.posts.data['1'].id).toBe(1) @@ -128,22 +126,20 @@ describe('Feature – Relations – Morphed By Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Tag.create({ - data: [ - { - id: 1, - name: 'news', - posts: [{ id: 1 }, { id: 2 }], - videos: [{ id: 3 }, { id: 4 }] - }, - { - id: 2, - name: 'cast', - posts: [{ id: 2 }, { id: 3 }], - videos: [{ id: 3 }, { id: 5 }] - } - ] - }) + await Tag.create([ + { + id: 1, + name: 'news', + posts: [{ id: 1 }, { id: 2 }], + videos: [{ id: 3 }, { id: 4 }] + }, + { + id: 2, + name: 'cast', + posts: [{ id: 2 }, { id: 3 }], + videos: [{ id: 3 }, { id: 5 }] + } + ]) expect(store.state.entities.posts.data['1'].id).toBe(1) expect(store.state.entities.posts.data['2'].id).toBe(2) @@ -232,12 +228,10 @@ describe('Feature – Relations – Morphed By Many – Persist', () => { const store = createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Tag.create({ - data: { - id: 1, - name: 'news', - posts: [{ id: 1, tag: { public: true } }, { id: 2 }], - videos: [{ id: 3, tag: { public: true } }, { id: 4 }] - } + id: 1, + name: 'news', + posts: [{ id: 1, tag: { public: true } }, { id: 2 }], + videos: [{ id: 3, tag: { public: true } }, { id: 4 }] }) expect(store.state.entities.posts.data['1'].id).toBe(1) diff --git a/test/feature/relations/MorphedByMany_Retrieve.spec.ts b/test/feature/relations/MorphedByMany_Retrieve.spec.ts index c5179922..83cb6502 100644 --- a/test/feature/relations/MorphedByMany_Retrieve.spec.ts +++ b/test/feature/relations/MorphedByMany_Retrieve.spec.ts @@ -92,26 +92,18 @@ describe('Feature – Relations – Morphed By Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [{ id: 1 }, { id: 5 }] - }) + await Post.create([{ id: 1 }, { id: 5 }]) - await Video.create({ - data: [{ id: 3 }, { id: 4 }, { id: 5 }] - }) + await Video.create([{ id: 3 }, { id: 4 }, { id: 5 }]) - await Tag.create({ - data: [{ id: 1, name: 'news' }, { id: 3, name: 'without references' }] - }) + await Tag.create([{ id: 1, name: 'news' }, { id: 3, name: 'without references' }]) - await Taggable.create({ - data: [ - { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts', is_public: false }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos', is_public: true }, - { id: 3, tag_id: 1, taggable_id: 5, taggable_type: 'posts', is_public: true }, - { id: 4, tag_id: 1, taggable_id: 4, taggable_type: 'videos', is_public: false } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts', is_public: false }, + { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos', is_public: true }, + { id: 3, tag_id: 1, taggable_id: 5, taggable_type: 'posts', is_public: true }, + { id: 4, tag_id: 1, taggable_id: 4, taggable_type: 'videos', is_public: false } + ]) const tag = Tag.query().with('posts').with('videos').find(1) as Tag @@ -210,26 +202,18 @@ describe('Feature – Relations – Morphed By Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [{ post_id: 1 }, { post_id: 5 }] - }) + await Post.create([{ post_id: 1 }, { post_id: 5 }]) - await Video.create({ - data: [{ id: 3 }, { id: 4 }, { id: 5 }] - }) + await Video.create([{ id: 3 }, { id: 4 }, { id: 5 }]) - await Tag.create({ - data: { tag_id: 1, name: 'news' } - }) + await Tag.create({ tag_id: 1, name: 'news' }) - await Taggable.create({ - data: [ - { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 1, taggable_id: 5, taggable_type: 'posts' }, - { id: 4, tag_id: 1, taggable_id: 4, taggable_type: 'videos' } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts' }, + { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, + { id: 3, tag_id: 1, taggable_id: 5, taggable_type: 'posts' }, + { id: 4, tag_id: 1, taggable_id: 4, taggable_type: 'videos' } + ]) const tag = Tag.query().with('posts').with('videos').find(1) as Tag @@ -324,29 +308,21 @@ describe('Feature – Relations – Morphed By Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) - await Post.create({ - data: [ - { id: 1, post_id: 100 }, - { id: 5, post_id: 105 } - ] - }) + await Post.create([ + { id: 1, post_id: 100 }, + { id: 5, post_id: 105 } + ]) - await Video.create({ - data: [{ id: 3 }, { id: 4 }, { id: 5 }] - }) + await Video.create([{ id: 3 }, { id: 4 }, { id: 5 }]) - await Tag.create({ - data: { id: 1, tag_id: 200, name: 'news' } - }) + await Tag.create({ id: 1, tag_id: 200, name: 'news' }) - await Taggable.create({ - data: [ - { id: 1, tag_id: 200, taggable_id: 100, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 200, taggable_id: 105, taggable_type: 'posts' }, - { id: 4, tag_id: 200, taggable_id: 4, taggable_type: 'videos' } - ] - }) + await Taggable.create([ + { id: 1, tag_id: 200, taggable_id: 100, taggable_type: 'posts' }, + { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, + { id: 3, tag_id: 200, taggable_id: 105, taggable_type: 'posts' }, + { id: 4, tag_id: 200, taggable_id: 4, taggable_type: 'videos' } + ]) const tag = Tag.query().with('posts').with('videos').find(1) as Tag @@ -430,11 +406,9 @@ describe('Feature – Relations – Morphed By Many – Retrieve', () => { createStore([{ model: Post }, { model: Video }, { model: Tag }, { model: Taggable }]) await Tag.create({ - data: { - id: 1, - posts: [{ id: 1 }, { id: 3 }, { id: 2 }], - videos: [{ id: 1 }, { id: 3 }, { id: 2 }] - } + id: 1, + posts: [{ id: 1 }, { id: 3 }, { id: 2 }], + videos: [{ id: 1 }, { id: 3 }, { id: 2 }] }) const tag = Tag.query() diff --git a/test/feature/relations/Persist_NestedMorph.spec.ts b/test/feature/relations/Persist_NestedMorph.spec.ts index a3966e22..acfbd299 100644 --- a/test/feature/relations/Persist_NestedMorph.spec.ts +++ b/test/feature/relations/Persist_NestedMorph.spec.ts @@ -40,19 +40,17 @@ describe('Features – Relations – Persist – Nested Morph', () => { const store = createStore([{ model: User }, { model: Post }, { model: Comment }]) - await store.dispatch('entities/users/create', { - data: { - id: 1, - comments: [ - { - id: 1, - user_id: 1, - commentable_id: 1, - commentable_type: 'posts', - commentable: { id: 1 } - } - ] - } + await User.create({ + id: 1, + comments: [ + { + id: 1, + user_id: 1, + commentable_id: 1, + commentable_type: 'posts', + commentable: { id: 1 } + } + ] }) const expected = createState({ diff --git a/test/feature/relations/Retrieve_Constraint.spec.ts b/test/feature/relations/Retrieve_Constraint.spec.ts index 6fc29675..230d6cfb 100644 --- a/test/feature/relations/Retrieve_Constraint.spec.ts +++ b/test/feature/relations/Retrieve_Constraint.spec.ts @@ -29,13 +29,13 @@ describe('Feature – Relations – Retrieve – Constraint', () => { it('can resolve relation constraint', async () => { const store = createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 1 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 1 }, + { id: 3, user_id: 2 } + ]) const expected = { $id: '1', diff --git a/test/feature/relations/Retrieve_DeepLoad.spec.ts b/test/feature/relations/Retrieve_DeepLoad.spec.ts index dbfb5235..1d3b9762 100644 --- a/test/feature/relations/Retrieve_DeepLoad.spec.ts +++ b/test/feature/relations/Retrieve_DeepLoad.spec.ts @@ -44,23 +44,21 @@ describe('Relations – Deep Load', () => { } } - const store = createStore([{ model: User }, { model: Post }, { model: Comment }]) + createStore([{ model: User }, { model: Post }, { model: Comment }]) - await store.dispatch('entities/posts/create', { - data: { + await Post.create({ + id: 1, + comments: [{ id: 1, - comments: [{ + user_id: 1, + user: { id: 1, - user_id: 1, - user: { - id: 1, - name: 'John Doe' - } - }] - } + name: 'John Doe' + } + }] }) - const post = store.getters['entities/posts/query']().withAll().first() + const post = Post.query().withAll().first() const expected = { $id: '1', diff --git a/test/feature/relations/Retrieve_Has.spec.ts b/test/feature/relations/Retrieve_Has.spec.ts index 6ca688f0..f5ba9359 100644 --- a/test/feature/relations/Retrieve_Has.spec.ts +++ b/test/feature/relations/Retrieve_Has.spec.ts @@ -27,37 +27,40 @@ describe('Feature – Relations – Retrieve – Has', () => { } it('can query data depending on relationship existence', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 2 } + ]) - const expected = [{ $id: '1', id: 1, posts: [] }, { $id: '2', id: 2, posts: [] }] + const expected = [ + { $id: '1', id: 1, posts: [] }, + { $id: '2', id: 2, posts: [] } + ] - const users = store.getters['entities/users/query']().has('posts').get() + const users = User.query().has('posts').get() expect(users).toEqual(expected) }) it('can query data depending on relationship absence', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 2 } + ]) const expected = [{ $id: '3', id: 3, posts: [] }] - const users = store.getters['entities/users/query']().hasNot('posts').get() + const users = User.query().hasNot('posts').get() expect(users).toEqual(expected) }) @@ -86,29 +89,27 @@ describe('Feature – Relations – Retrieve – Has', () => { } } - const store = createStore([{ model: User }, { model: Phone }, { model: Post }]) + createStore([{ model: User }, { model: Phone }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 2 } + ]) - await store.dispatch('entities/phones/create', { - data: [{ id: 1, user_id: 2 }] - }) + await Phone.create([{ id: 1, user_id: 2 }]) const expected1 = [{ $id: '1', id: 1, phone: null, posts: [] }] const expected2 = [{ $id: '2', id: 2, phone: null, posts: [] }] - const users1 = store.getters['entities/users/query']() - .whereHas('posts', (query: Query) => query.where('id', 1)) + const users1 = User.query() + .whereHas('posts', (query: Query) => { query.where('id', 1) }) .get() - const users2 = store.getters['entities/users/query']() - .whereHas('phone', (query: Query) => query.where('id', 1)) + const users2 = User.query() + .whereHas('phone', (query: Query) => { query.where('id', 1) }) .get() expect(users1).toEqual(expected1) @@ -116,57 +117,63 @@ describe('Feature – Relations – Retrieve – Has', () => { }) it('can query data depending on relationship absence with constraint', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 2 } + ]) - const expected = [{ $id: '2', id: 2, posts: [] }, { $id: '3', id: 3, posts: [] }] + const expected = [ + { $id: '2', id: 2, posts: [] }, + { $id: '3', id: 3, posts: [] } + ] - const users = store.getters['entities/users/query']() - .whereHasNot('posts', (query: Query) => query.where('id', 1)) + const users = User.query() + .whereHasNot('posts', (query: Query) => { query.where('id', 1) }) .get() expect(users).toEqual(expected) }) it('can query data depending on relationship existence with number', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 2 } + ]) const expected = [{ $id: '2', id: 2, posts: [] }] - const users = store.getters['entities/users/query']().has('posts', 2).get() + const users = User.query().has('posts', 2).get() expect(users).toEqual(expected) }) it('can query data depending on relationship absence with number', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([ + { id: 1, user_id: 1 }, + { id: 2, user_id: 2 }, + { id: 3, user_id: 2 } + ]) - const expected = [{ $id: '1', id: 1, posts: [] }, { $id: '3', id: 3, posts: [] }] + const expected = [ + { $id: '1', id: 1, posts: [] }, + { $id: '3', id: 3, posts: [] } + ] - const users = store.getters['entities/users/query']().hasNot('posts', 2).get() + const users = User.query().hasNot('posts', 2).get() expect(users).toEqual(expected) }) @@ -195,45 +202,35 @@ describe('Feature – Relations – Retrieve – Has', () => { } } - const store = createStore([{ model: User }, { model: Phone }, { model: Post }]) + createStore([{ model: User }, { model: Phone }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }]) - await store.dispatch('entities/phones/create', { - data: [{ id: 1, user_id: 1 }] - }) + await Phone.create([{ id: 1, user_id: 1 }]) - expect(store.getters['entities/users/query']().has('posts', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }, { $id: '2', id: 2, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('posts', '=', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('posts', '>', 1).get()).toEqual([{ $id: '2', id: 2, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('posts', '>=', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }, { $id: '2', id: 2, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('posts', '<', 2).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('posts', '<=', 2).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }, { $id: '2', id: 2, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('posts', 'unknown', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) + expect(User.query().has('posts', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }, { $id: '2', id: 2, phone: null, posts: [] }]) + expect(User.query().has('posts', '=', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) + expect(User.query().has('posts', '>', 1).get()).toEqual([{ $id: '2', id: 2, phone: null, posts: [] }]) + expect(User.query().has('posts', '>=', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }, { $id: '2', id: 2, phone: null, posts: [] }]) + expect(User.query().has('posts', '<', 2).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) + expect(User.query().has('posts', '<=', 2).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }, { $id: '2', id: 2, phone: null, posts: [] }]) + expect(User.query().has('posts', 'unknown', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - expect(store.getters['entities/users/query']().has('phone', '=', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) + expect(User.query().has('phone', '=', 1).get()).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) }) it('can query data depending on relationship absence with string conditions', async () => { - const store = createStore([{ model: User }, { model: Post }]) + createStore([{ model: User }, { model: Post }]) - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) + await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - await store.dispatch('entities/posts/create', { - data: [{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }] - }) + await Post.create([{ id: 1, user_id: 1 }, { id: 2, user_id: 2 }, { id: 3, user_id: 2 }]) - expect(store.getters['entities/users/query']().hasNot('posts', '>', 1).get()).toEqual([{ $id: '1', id: 1, posts: [] }, { $id: '3', id: 3, posts: [] }]) - expect(store.getters['entities/users/query']().hasNot('posts', '>=', 1).get()).toEqual([{ $id: '3', id: 3, posts: [] }]) - expect(store.getters['entities/users/query']().hasNot('posts', '<', 2).get()).toEqual([{ $id: '2', id: 2, posts: [] }, { $id: '3', id: 3, posts: [] }]) - expect(store.getters['entities/users/query']().hasNot('posts', '<=', 2).get()).toEqual([{ $id: '3', id: 3, posts: [] }]) + expect(User.query().hasNot('posts', '>', 1).get()).toEqual([{ $id: '1', id: 1, posts: [] }, { $id: '3', id: 3, posts: [] }]) + expect(User.query().hasNot('posts', '>=', 1).get()).toEqual([{ $id: '3', id: 3, posts: [] }]) + expect(User.query().hasNot('posts', '<', 2).get()).toEqual([{ $id: '2', id: 2, posts: [] }, { $id: '3', id: 3, posts: [] }]) + expect(User.query().hasNot('posts', '<=', 2).get()).toEqual([{ $id: '3', id: 3, posts: [] }]) }) }) diff --git a/test/feature/relations/Retrieve_SelfRelation.spec.ts b/test/feature/relations/Retrieve_SelfRelation.spec.ts index 0c53a82a..9e7ecde6 100644 --- a/test/feature/relations/Retrieve_SelfRelation.spec.ts +++ b/test/feature/relations/Retrieve_SelfRelation.spec.ts @@ -27,13 +27,8 @@ describe('Feature – Relations – Retrieve – Self Relation', () => { createStore([{ model: Post }]) - await Post.insert({ - data: { id: 1, parent_id: null } - }) - - await Post.insert({ - data: { id: 2, parent_id: 1 } - }) + await Post.insert({ id: 1, parent_id: null }) + await Post.insert({ id: 2, parent_id: 1 }) const post = Post.query().with('parent').find(2) as Post diff --git a/test/feature/relations/Retrieve_StringDefine.spec.ts b/test/feature/relations/Retrieve_StringDefine.spec.ts index 4c51b839..2c039889 100644 --- a/test/feature/relations/Retrieve_StringDefine.spec.ts +++ b/test/feature/relations/Retrieve_StringDefine.spec.ts @@ -40,13 +40,9 @@ describe('Feature – Relations - String Define', () => { createStore([{ model: User }, { model: Post }]) - await User.create({ - data: { id: 1 } - }) + await User.create({ id: 1 }) - await Post.create({ - data: { id: 2, user_id: 1 } - }) + await Post.create({ id: 2, user_id: 1 }) const user = User.query().with('posts').find(1) as User diff --git a/test/feature/relations/Retrieve_With.spec.ts b/test/feature/relations/Retrieve_With.spec.ts index 03507cd5..e2e4d8a5 100644 --- a/test/feature/relations/Retrieve_With.spec.ts +++ b/test/feature/relations/Retrieve_With.spec.ts @@ -62,14 +62,12 @@ describe('Feature – Relations – Retrieve – With', () => { createStore([{ model: User }, { model: Phone }, { model: Post }]) await User.create({ - data: { - id: 1, - phone: { id: 2, user_id: 1 }, - posts: [ - { id: 3, user_id: 1 }, - { id: 4, user_id: 1 } - ] - } + id: 1, + phone: { id: 2, user_id: 1 }, + posts: [ + { id: 3, user_id: 1 }, + { id: 4, user_id: 1 } + ] }) const user1 = User.query().with('*').first() as User @@ -148,22 +146,20 @@ describe('Feature – Relations – Retrieve – With', () => { createStore([{ model: User }, { model: Phone }, { model: Post }]) await User.create({ - data: { - id: 1, - phone: { id: 2, user_id: 1 }, - posts: [ - { - id: 3, - user_id: 1, - user: { id: 1 } - }, - { - id: 4, - user_id: 1, - user: { id: 1 } - } - ] - } + id: 1, + phone: { id: 2, user_id: 1 }, + posts: [ + { + id: 3, + user_id: 1, + user: { id: 1 } + }, + { + id: 4, + user_id: 1, + user: { id: 1 } + } + ] }) const user1 = User.query().withAllRecursive().first() as User @@ -239,30 +235,28 @@ describe('Feature – Relations – Retrieve – With', () => { } } - const store = createStore([{ model: User }, { model: Post }, { model: Comment }]) + createStore([{ model: User }, { model: Post }, { model: Comment }]) - await store.dispatch('entities/users/create', { - data: { - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ] - } - ] - } + await User.create({ + id: 1, + posts: [ + { + id: 1, + user_id: 1, + comments: [ + { id: 1, post_id: 1 }, + { id: 2, post_id: 1 } + ] + }, + { + id: 2, + user_id: 1, + comments: [ + { id: 3, post_id: 2 }, + { id: 4, post_id: 2 } + ] + } + ] }) const expected = { @@ -351,28 +345,26 @@ describe('Feature – Relations – Retrieve – With', () => { } } - const store = createStore([{ model: User }, { model: Post }, { model: Comment }]) + createStore([{ model: User }, { model: Post }, { model: Comment }]) - await store.dispatch('entities/users/create', { - data: [ - { - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - } - ] - }, - { - id: 2 - } - ] - }) + await User.create([ + { + id: 1, + posts: [ + { + id: 1, + user_id: 1, + comments: [ + { id: 1, post_id: 1 }, + { id: 2, post_id: 1 } + ] + } + ] + }, + { + id: 2 + } + ]) const expected = [ { @@ -397,9 +389,7 @@ describe('Feature – Relations – Retrieve – With', () => { } ] - const users = await store.getters['entities/users/query']() - .with('posts.comments') - .findIn([1, 2]) as User[] + const users = User.query().with('posts.comments').findIn([1, 2]) expect(users).toEqual(expected) }) @@ -481,30 +471,28 @@ describe('Feature – Relations – Retrieve – With', () => { } } - const store = createStore([{ model: User }, { model: Post }, { model: Comment }, { model: Like }]) + createStore([{ model: User }, { model: Post }, { model: Comment }, { model: Like }]) - await store.dispatch('entities/users/create', { - data: { - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1, likes: [{ id: 1, comment_id: 1 }] }, - { id: 2, post_id: 1, likes: [{ id: 2, comment_id: 2 }] } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2, likes: [{ id: 3, comment_id: 3 }] }, - { id: 4, post_id: 2, likes: [{ id: 4, comment_id: 4 }] } - ] - } - ] - } + await User.create({ + id: 1, + posts: [ + { + id: 1, + user_id: 1, + comments: [ + { id: 1, post_id: 1, likes: [{ id: 1, comment_id: 1 }] }, + { id: 2, post_id: 1, likes: [{ id: 2, comment_id: 2 }] } + ] + }, + { + id: 2, + user_id: 1, + comments: [ + { id: 3, post_id: 2, likes: [{ id: 3, comment_id: 3 }] }, + { id: 4, post_id: 2, likes: [{ id: 4, comment_id: 4 }] } + ] + } + ] }) const expected = { @@ -532,7 +520,7 @@ describe('Feature – Relations – Retrieve – With', () => { ] } - const users = store.getters['entities/users/query']().with('posts.comments.likes').find(1) as User[] + const users = User.query().with('posts.comments.likes').find(1) expect(users).toEqual(expected) }) @@ -614,38 +602,36 @@ describe('Feature – Relations – Retrieve – With', () => { } } - const store = createStore([{ model: User }, { model: Post }, { model: Comment }, { model: Like }]) + createStore([{ model: User }, { model: Post }, { model: Comment }, { model: Like }]) - await store.dispatch('entities/users/create', { - data: { - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ], - likes: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ], - likes: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ] - } - ] - } + await User.create({ + id: 1, + posts: [ + { + id: 1, + user_id: 1, + comments: [ + { id: 1, post_id: 1 }, + { id: 2, post_id: 1 } + ], + likes: [ + { id: 1, post_id: 1 }, + { id: 2, post_id: 1 } + ] + }, + { + id: 2, + user_id: 1, + comments: [ + { id: 3, post_id: 2 }, + { id: 4, post_id: 2 } + ], + likes: [ + { id: 3, post_id: 2 }, + { id: 4, post_id: 2 } + ] + } + ] }) const expected = { @@ -681,8 +667,8 @@ describe('Feature – Relations – Retrieve – With', () => { ] } - const user1 = User.query().with('posts.comments|likes').find(1) as User - const user2 = User.query().with('posts.*').find(1) as User + const user1 = User.query().with('posts.comments|likes').find(1) + const user2 = User.query().with('posts.*').find(1) expect(user1).toEqual(expected) expect(user2).toEqual(expected) @@ -735,38 +721,36 @@ describe('Feature – Relations – Retrieve – With', () => { } } - const store = createStore([{ model: User }, { model: Post }, { model: Comment }, { model: Like }]) + createStore([{ model: User }, { model: Post }, { model: Comment }, { model: Like }]) - await store.dispatch('entities/users/create', { - data: { - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ], - likes: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ], - likes: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ] - } - ] - } + await User.create({ + id: 1, + posts: [ + { + id: 1, + user_id: 1, + comments: [ + { id: 1, post_id: 1 }, + { id: 2, post_id: 1 } + ], + likes: [ + { id: 1, post_id: 1 }, + { id: 2, post_id: 1 } + ] + }, + { + id: 2, + user_id: 1, + comments: [ + { id: 3, post_id: 2 }, + { id: 4, post_id: 2 } + ], + likes: [ + { id: 3, post_id: 2 }, + { id: 4, post_id: 2 } + ] + } + ] }) const expected = { @@ -802,8 +786,8 @@ describe('Feature – Relations – Retrieve – With', () => { ] } - const user1 = store.getters['entities/users/query']().with(['posts.comments', 'posts.likes']).find(1) - const user2 = store.getters['entities/users/query']().with(['posts.*']).find(1) + const user1 = User.query().with(['posts.comments', 'posts.likes']).find(1) + const user2 = User.query().with(['posts.*']).find(1) expect(user1).toEqual(expected) expect(user2).toEqual(expected) @@ -847,12 +831,10 @@ describe('Feature – Relations – Retrieve – With', () => { createStore([{ model: User }, { model: Post }]) await User.create({ - data: { - id: 1, - posts: [ - { id: 1, user_id: 1 } - ] - } + id: 1, + posts: [ + { id: 1, user_id: 1 } + ] }) const user = User.query().with('unknown').first() as User @@ -897,17 +879,9 @@ describe('Feature – Relations – Retrieve – With', () => { createStore([{ model: User }, { model: Post }]) - await User.create({ - data: { - id: 1 - } - }) - await Post.create({ - data: { - id: 1, - user_id: null - } - }) + await User.create({ id: 1 }) + + await Post.create({ id: 1, user_id: null }) const post = Post.query() .with('*') diff --git a/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts b/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts index a760a87a..150f7b7d 100644 --- a/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts +++ b/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts @@ -67,17 +67,15 @@ describe('Feature – Relations – With – Nested Constraints', () => { }) it('can apply `with` constraints to nested relations', async () => { - await User.create({ data: { id: 1 } }) + await User.create({ id: 1 }) - await Post.create({ data: { id: 1, user_id: 1 } }) + await Post.create({ id: 1, user_id: 1 }) - await Comment.create({ - data: [ - { id: 1, post_id: 1, title: 'Title01' }, - { id: 2, post_id: 1, title: 'Title01' }, - { id: 3, post_id: 1, title: 'Title02' } - ] - }) + await Comment.create([ + { id: 1, post_id: 1, title: 'Title01' }, + { id: 2, post_id: 1, title: 'Title01' }, + { id: 3, post_id: 1, title: 'Title02' } + ]) const user = User.query().with('posts.comments', (query) => { query.where('title', 'Title01') From 3ba4c7dfd1a2b7e500731d2f8462dfa90d93aaa3 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Wed, 1 Apr 2020 09:44:18 +0100 Subject: [PATCH 03/56] docs: api reference revision (#610) --- docs/.vuepress/config.js | 13 +- docs/api/container/container.md | 12 +- docs/api/database/database.md | 139 ++---- docs/api/deleting-data.md | 145 ------ docs/api/model/attributes.md | 294 +++++++++++ docs/api/model/lifecycle-hooks.md | 105 ++++ docs/api/model/model.md | 483 +++++++++++++++---- docs/api/query/query.md | 251 ++++++++++ docs/guide/digging-deeper/vuex-module.md | 2 +- docs/guide/model/single-table-inheritance.md | 64 ++- 10 files changed, 1135 insertions(+), 373 deletions(-) delete mode 100644 docs/api/deleting-data.md create mode 100644 docs/api/model/attributes.md create mode 100644 docs/api/model/lifecycle-hooks.md create mode 100644 docs/api/query/query.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 62071bfb..fbfcf48b 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -69,7 +69,16 @@ const sidebars = { title: 'Model', collapsable: false, children: [ - '/api/model/model' + '/api/model/model', + '/api/model/attributes', + '/api/model/lifecycle-hooks' + ] + }, + { + title: 'Query', + collapsable: false, + children: [ + '/api/query/query' ] } ] @@ -77,7 +86,7 @@ const sidebars = { module.exports = { title: 'Vuex ORM', - description: 'The Vuex plugin to enable Object-Relational Mapping access to the Vuex Store.', + description: 'The Vuex plugin to enable Object-Relational Mapping (ORM) access to the Vuex Store.', base: '/vuex-orm/', diff --git a/docs/api/container/container.md b/docs/api/container/container.md index 8688f58c..7d49ce42 100644 --- a/docs/api/container/container.md +++ b/docs/api/container/container.md @@ -4,13 +4,13 @@ sidebarDepth: 2 # Container -The Container is the global object that holds Vuex Store instance. The only purpose of the Container is to hold store instance so that we can use it in other places. Please refer to [Database](/api/database/database) to see why we need this global object in the first place. +The Container holds the Vuex Store instance. The only purpose of the Container is to hold store instance so that it can be used throughout the library. It is tightly coupled with the [Database](/api/database/database) instance. ## Static Properties -### store +### `store` -- **`static store: Vuex.Store`** +- **Type**: `Vuex.Store` The store instance that Vuex ORM is being installed. @@ -18,12 +18,12 @@ The Container is the global object that holds Vuex Store instance. The only purp ### register -- **`static register(store: Vuex.Store): void`** +- **Type**: `(store: Vuex.Store) => void` Register a store instance to the Container. ```js - Container.register(store) + Container.register(new Vuex.Store()) ``` - This method is called during the Vuex plugin installation, so usually, you would never have to use this method. + This method is called during the Vuex plugin installation. diff --git a/docs/api/database/database.md b/docs/api/database/database.md index f26fa97c..a20e3126 100644 --- a/docs/api/database/database.md +++ b/docs/api/database/database.md @@ -4,45 +4,13 @@ sidebarDepth: 2 # Database -The Database is the object that holds all Models and Modules that are registered to the Vuex ORM. It is also responsible for generating the whole database relational schema from registered models. This schema is used to "Normalize" data before persisting to the Vuex Store. +The Database holds all models and modules registered to Vuex ORM. It is also responsible for generating the database relational schema from the registered models. This schema is used to "Normalize" data before persisting to the Vuex Store. -When using Vuex ORM, you will unlikely need to use the Database class after it's registered to the Vuex Store. But for those who are curious, we'll describe why the Database object exists in the first place. +In Vuex ORM, models may have relations with other models. To resolve those relations, models require some form of tracking in order to communicate with one another. -In Vuex ORM, any Model can have any type of relationship with other Models. To resolve those relationships, we need to store all Models somewhere so that a Model can reference each other. That's where the Database comes in to play. You can get any registered Model like this. +The Database is designed to track all registered models, build relational schema from those models, and register the database to the Vuex Store instance (contained in [Container](/api/container/container)). -```js -const database = new Database() - -database.register(User) - -const user = database.model('users') -``` - -You might wonder why do we need to store all Models in one place since the related Models are passed at Model when defining relationships like below. - -```js -import { Model } from '@vuex-orm/core' -import Post from './Post' - -class User extends Model { - static entity = 'users' - - static fields () { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') // <- Passing related Model. - } - } -} -``` - -So, can't we just resolve relationship directly from the Model? Unfortunately no, we can't. The primary reason is that Vuex ORM is built on top of Vuex, and Vuex ORM is calling Vuex Getters/Actions/Mutations to interact with the Vuex Store. In fact, you can call Vuex Actions directly to create or fetch data. - -Vuex Module doesn't have access to Model. It must resolve the Model from the entity name, which is a `string`. When a user calls actions like `store.dispatch('entities/users/insert', { ... })`, we must somehow get User Model by the namespace, which is `users` in `entities/users/insert`. Well, Vuex ORM actions are getting Models from the Database. - -Finally, the created Database instance is registered to the Vuex Store instance, then it's registered to the [Container](../container/container) so we have access to it from everywhere. - -You can access the database instance through the store instance, or Container. +The database instance can be accessed through the store instance, or [Container](/api/container/container). ```js // Through the store instance. @@ -54,77 +22,69 @@ import { Container } from '@vuex-orm/core' Container.store.$db() ``` + ## Instance Properties -### store +### `store` -- **`store!: Vuex.Store`** +- **Type**: `Vuex.Store` The Vuex Store instance. -### namespace +### `namespace` -- **`namespace!: string`** +- **Type**: `string = 'entities'` - The namespace of the Vuex Store Module where all entities are registered under. the default is `entities`. + The namespace of the Vuex Store module where all entities are registered under. + + The default namespace is `entities` and can be configured during setup. -### entities +- **See also**: [Changing the Namespace](/guide/model/database-registration.md#changing-the-namespace) -- **`entities: Entity[] = []`** +### `entities` - The list of entities registered to the Vuex Store. It contains models and modules with its name. The Entity interface looks like below. +- **Type**: `Array` - ```ts - interface Entity { - name: string - base: string - model: typeof Model - module: Vuex.Module - } - ``` + The collection of entities registered to the Vuex Store. It contains references to the models and corresponding modules. -### schema +### `schema` -- **`schemas: Schemas = {}`** +- **Type**: `Object` - The database schema definition. This schema is going to be used when normalizing the data before persisting them to the Vuex Store. Schemas interface is a list of Normalizr schema. + The database schema definition. This schema is used when normalizing data before persisting it to the Vuex Store. - ```ts - interface Schemas { - [entity: string]: NormalizrSchema.Entity - } - ``` ## Instance Methods -### register +### `register` -- **`register(model: typeof Model, module: Vuex.Module = {}): void`** +- **Type**: `(model: Model, module: Object) => void` - Register a model and a module to Database. + Register a model and a Vuex module to the Database. ```js - database.register(User, users) - ``` + const database = new Database() - You can omit registering a module. + // With a Vuex module. + database.register(User, users) - ```js + // Without a Vuex module. database.register(User) ``` -### start +### `start` -- **`start (store: Vuex.Store, namespace: string): void`** +- **Type**: `(store: Vuex.Store, namespace: string) => void` - This method will generate Vuex Module and Normalizr schema tree from the registered Models. It will be called when adding Vuex ORM to Vuex as a plugin. + Generate Vuex Module and Normalizr schema tree from the registered models. This method is invoked when adding Vuex ORM to Vuex as a plugin. -### model +### `model` -- **`model(model: T): T`**
- **`model(model: string): typeof Model`** +- **Type**: `(model: Model | string): Model` - Get the model of the given name from the entities list. It is going to through error if the model was not found. + Get the model by entity from the entities list. If a model is passed as the argument, then the model [entity](/api/model/model.md#entity) will be used. + + Throws an error if the model is not found. ```js const user = database.model('users') @@ -132,11 +92,11 @@ Container.store.$db() // User ``` -### models +### `models` -- **`models (): { [name: string]: typeof Model }`** +- **Type**: `() => Object` - Get all models from the entities list. The result will be object with key being the entity name for the Model. + Get all models from the entities list. The result will be a plain object with key being the entity name for the model. ```js const models = database.model() @@ -144,21 +104,26 @@ Container.store.$db() // { users: User, posts: Post } ``` -### baseModel +### `baseModel` -- **`baseModel(model: T): T`**
- **`baseModel(model: string): typeof Model`** +- **Type**: `(model: Model | string) => Model` - Get the base model of the given name from the entities list. The base Model is only relevant when the model is inheriting another model to achieve Single Table inheritance feature. It is going to through error if the model was not found. + Get the base model by entity from the entities list. If a model is passed as the argument, then the model [baseEntity](/api/model/model.md#baseentity) will be used. + + The `baseModel` is only relevant when the model is inheriting another model to achieve Single Table Inheritance. + + Throws an error if the model is not found. -### module +### `module` + +- **Type**: `(name: string) => Vuex.Module` -- **`module (name: string): Vuex.Module`** + Get the module by entity from the entities list. - Get the module of the given name from the entities list. It is going to through error if the module was not found. + Throws an error if the model is not found. -### modules +### `modules` -- **`modules (): { [name: string]: Vuex.Module }`** +- **Type**: `() => Object` - Get all modules from the entities list. The result will be object with key being the entity name for the Module. + Get all modules from the entities list. The result will be a plain object with key being the entity name for the module. diff --git a/docs/api/deleting-data.md b/docs/api/deleting-data.md deleted file mode 100644 index 892150c5..00000000 --- a/docs/api/deleting-data.md +++ /dev/null @@ -1,145 +0,0 @@ -# Deleting Data - -You can delete data from the store by calling the delete Method on the model class or dispatching the `delete` action. Both expecting the first argument to be `String`, `Number`, `Function` or `Object`. - -If you use `String` or `Number`, a record that matches the condition with its primary key is going to be deleted. - -If you use `Function`, that function is going to be used to determine which record to delete. The function takes the record as the argument and must return boolean. - -By passing in an `Object` as argument the object is expacted to have a `where` key which have to be `String`, `Number` or `Function` exactly like described above. - -## Delete Data By Primary Key Value - -```js -// Initial state. -let state = { - entities: { - users: { - '1': { id: 1, name: 'John' }, - '2': { id: 1, name: 'Jane' } - } - } -} - - -// Delete single data by primary key value with model class. -User.delete(1); - -// Or you can pass obejct as argument as well. -User.delete({ where: 1 }) - -// Or you can delete data from an existing model instance. -const user = await User.find(1) -user.delete() - -// Or you can delete single data by primary key value with vuex action. -store.dispatch('entities/users/delete', 1) - -// Or you can pass obejct as argument as well. -store.dispatch('entities/users/delete', { where: 1 }) - -// State after `delete` -state = { - entities: { - users: { - '2': { id: 1, name: 'Jane' } - } - } -} -``` - -## Delete Data By Closure - -```js -// Initial state. -let state = { - entities: { - users: { - '1': { id: 1, name: 'John' }, - '2': { id: 1, name: 'Jane' }, - '3': { id: 1, name: 'George' } - }, - posts: { - '1': { id: 1, user_id: 1 }, - '2': { id: 2, user_id: 2 }, - '3': { id: 3, user_id: 3 } - } - } -} - -// Delete data by closure. -User.delete((record) => { - return record.id === 1 || record.name === 'Jane' -}) - -// Or with object style. -User.delete({ - where (record) { - return record.id === 1 || record.name === 'Jane' - } -}) - -// State after `delete`. -state = { - entities: { - users: { - '3': { id: 1, name: 'George' } - }, - posts: { - '1': { id: 1, user_id: 1 }, - '2': { id: 2, user_id: 2 }, - '3': { id: 3, user_id: 3 } - } - } -} -``` - -## Delete All Data - -You can delete all data in once by `deleteAll` action. - -```js -// Delete all data for an entity -User.deleteAll() - -// State after `deleteAll`. -let state = { - entities: { - users: {}, - posts: { - '1': { id: 1, user_id: 1 }, - '2': { id: 2, user_id: 2 }, - '3': { id: 3, user_id: 3 } - } - } -} - -// Delete all data for all entities -store.dispatch('entities/deleteAll') - -// State after `deleteAll`. -state = { - entities: { - users: {}, - posts: {} - } -} -``` - -## Dispatch Action From Root Module - -You can also dispatch action from root module. Remember to pass `entity` name in this case. - -```js -store.dispatch('entities/delete', { - entity: 'users', - where: 1 -}) -``` - -Note that since root module needs `entity`, you can't dispatch root module action by passing condition directly. - -```js -// This would not work. -store.dispatch('entities/delete', 1) -``` diff --git a/docs/api/model/attributes.md b/docs/api/model/attributes.md new file mode 100644 index 00000000..44b9ef27 --- /dev/null +++ b/docs/api/model/attributes.md @@ -0,0 +1,294 @@ +# Attributes + +## Field Attributes + +Field attributes aid in the handling of default values and casting. Generic +types, primitive types and UID types are supported. + +Any field types that allow `null` values may be chained with `nullable()` to +prevent casting. For example, a null value on a number attribute will resolve +as `0` which may be an undesired outcome. + +```js +{ + active: this.number(1).nullable() +} +``` + +**See also**: [Defining Models: Field Attributes](/guide/model/defining-models.md#field-attributes) + +### `attr` + +- **Type**: `(value: any, mutator?: () => any) => Object` + + Create a generic attribute. Any value can be passed as the default value. + + A closure may also be passed: + + ```js + { + rand: this.attr(() => Math.random()) + } + ``` + +### `string` + +- **Type**: `(value: any, mutator?: () => string | null) => Object` + + Create a string attribute. Values will be cast to [String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String). + +### `number` + +- **Type**: `(value: any, mutator?: () => number | null) => Object` + + Create a number attribute. Values will be cast to [Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number). + +### `boolean` + +- **Type**: `(value: any, mutator?: () => boolean | null) => Object` + + Create a boolean attribute. Values will be cast to [Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean). + +### `uid` + +- **Type**: `(value?: () => string | number) => Object` + + Create a Unique ID attribute. When a field of this type is omitted it will + generate an auto-incremented number with a `$uid` prefix (e.g. `$uid32`). + + A closure may also be passed: + + ```js + import { v4 as uuidv4 } from 'uuid' + + { + id: this.uid(() => uuidv4()) // '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' + } + ``` + + +## Relation Attributes + +### `hasOne` + +- **Type**: `(related: Model | string, foreignKey: string, localKey?: string) => Object` + + Create a 1:1 relation where two models may have only one record on + either side of the relationship connected by a single foreign key. + For example, a `User` may own a single `Post`. + + This relation may be reversed using the [belongsTo](#belongsto) attribute. + + | Arguments | | + |---------------|----------------------------------------------------------------| + | `related`* | The target model e.g. `Post` or `'posts'`. | + | `foreignKey`* | The foreign key on the source model e.g. `user_id`. | + | `localKey` | The custom key on the source model it's foreign key points to. | + +- **See also**: [Relationships: One To One](/guide/model/relationships.md#one-to-one) + +### `belongsTo` + +- **Type**: `(parent: Model | string, foreignKey: string, ownerKey?: string) => Object` + + Create a one-to-one/many inverse relation where a single record + belongs to one or more source models connected by a single foreign key. + For example, a single `Profile` may belong to one or more `User` records. + + This relation attribute may be used to create an inverse relation of + [hasOne](#hasone) and [hasMany](#hasmany). + + | Arguments | | + |---------------|----------------------------------------------------------------| + | `parent`* | The source model e.g. `User` or `'users'`. | + | `foreignKey`* | The foreign key on the target model e.g. `user_id`. | + | `ownerKey` | The custom key on the target model it's foreign key points to. | + +- **See also**: [Relationships: One To One Inverse](/guide/model/relationships.md#one-to-one-inverse), [One To Many Inverse](/guide/model/relationships.md#one-to-many) + +### `hasMany` + +- **Type**: `(related: Model | string, foreignKey: string, localKey?: string) => Object` + + Create a one-to-many relation where a single model may own any amount of + records belonging to another model. + For example, a `Post` may have an infinite number of `Comment` records. + + | Arguments | | + |---------------|----------------------------------------------------------------| + | `related`* | The target model e.g. `Comment` or `'comments'`. | + | `foreignKey`* | The foreign key on the source model e.g. `post_id`. | + | `localKey` | The custom key on the source model it's foreign key points to. | + + This relation may be reversed using the [belongsTo](#belongsto) attribute. + +- **See also**: [Relationships: One To Many](/guide/model/relationships.md#one-to-many) + +### `hasManyBy` + +- **Type**: `(parent: Model | string, foreignKey: string, ownerKey?: string) => Object` + + Create a one-to-many relation where a single model may own many records on + another model by declaring a field on the target model that contains an array + of foreign keys to the source model. + For example, a `Cluster` may own any amount of `Node` records who's foreign + keys are declared as an array on the `Cluster` model. + + | Arguments | | + |---------------|-----------------------------------------------------------------| + | `parent`* | The source model e.g. `Node` or `'nodes'`. | + | `foreignKey`* | The foreign keys on the target model e.g. `node_ids`. | + | `ownerKey` | The custom key on the source model it's foreign keys points to. | + +- **See also**: [Relationships: Has Many By](/guide/model/relationships.md#has-many-by) + +### `hasManyThrough` + +- **Type**: `(related: Model | string, through: Model | string, firstKey: string, secondKey: string, localKey?: string, secondLocalKey?: string) => Object` + + Create a one-to-many intermediate relation where a single model can own any + amount of records attached to another model through an intermediate relation. + For example, a `Country` can have many distant `Post` records through a `User`. + The `User` may belong to a `Country` and the `Post` records belong to a `User`. + + | Arguments | | + |------------------|----------------------------------------------------------------------| + | `related`* | The source model e.g. `Post` or `'posts'`. | + | `through`* | The intermediate model e.g. `User` or `'users'` | + | `firstKey`* | The foreign key on the intermediate model e.g. `country_id`. | + | `secondKey`* | The foreign key on the source model e.g. `user_id` | + | `localKey` | The custom key on the target model it's foreign key points to. | + | `secondLocalKey` | The custom key on the intermediate model it's foreign key points to. | + +- **See also**: [Relationships: Has Many Through](/guide/model/relationships.md#has-many-through) + +### `belongsToMany` + +- **Type**: `(related: Model | string, pivot: Model | string, foreignPivotKey: string, relatedPivotKey: string, parentKey?: string, relatedKey?: string) => Object` + + Create a many-to-many relation where two models may own any amount of records + on either side of the relationship connected by an intermediate model. + For example, a `User` may own many `Role` records and `Role` may belong to + many `User` records. + + The intermediate model connects the two models by a foreign key on the source + and target model. + + | Arguments | | + |--------------------|----------------------------------------------------------------------| + | `related`* | The source model e.g. `Role` or `'roles'`. | + | `pivot`* | The intermediate model e.g. `RoleUser` or `'roleUser'` | + | `foreignPivotKey`* | The foreign key on the target model e.g. `user_id`. | + | `relatedPivotKey`* | The foreign key on the source model e.g. `role_id` | + | `parentKey` | The custom key on the target model it's foreign key points to. | + | `relatedKey` | The custom key on the intermediate model it's foreign key points to. | + +- **See also**: [Relationships: Many To Many](/guide/model/relationships.md#many-to-many) + +### `morphOne` + +- **Type**: `(related: Model | string, id: string, type: string, localKey?: string) => Object` + + Create a one-to-one polymorphic relation where a single model may own a single + record on more than one type of model connected by a single target model. + For example, `User` and `Post` may own a single `Comment` scoped through + `Commentable`. + + This relation attribute behaves similar to [hasOne](#hasone) but without + the model type constraint. + + | Arguments | | + |------------|---------------------------------------------------------------------------| + | `related`* | The target model e.g. `Comment` or `'comments'`. | + | `id`* | The attribute to hold the target model foreign key e.g. `commentable_id`. | + | `type`* | The attribute to hold the target model entity e.g. `commentable_type`. | + | `localKey` | The custom key on the target model it's foreign key points to. | + +- **See also**: [Relationships: One To One (Polymorphic)](/guide/model/relationships.md#one-to-one-polymorphic) + +### `morphTo` + +- **Type**: `(id: string, type: string) => Object` + + Create a one-to-one or one-to-many polymorphic inverse relation where a + single record may belong to more than one type of model connected by a single + target model. + For example, a `Comment` may belong to `User` and `Post` scoped through + `Commentable`. + + This relation attribute behaves similar to [belongsTo](#belongsto) but without + the model type constraint. It may be used to create an inverse relation + of [morphOne](#morphone) and [morphMany](#morphmany). + + | Arguments | | + |-----------|---------------------------------------------------------------------------| + | `id`* | The attribute holding the source model foreign key e.g. `commentable_id`. | + | `type`* | The attribute holding the source model entity e.g. `commentable_type`. | + +- **See also**: [Relationships: One To One (Polymorphic)](/guide/model/relationships.md#one-to-one-polymorphic) + +### `morphMany` + +- **Type**: `(related: Model | string, id: string, type: string, localKey?: string) => Object` + + Create a one-to-many polymorphic relation where models of more than one type + may own many records on a single model connected by a single target model. + For example, `Video` and `Post` may own an infinite number of `Comment` records + scoped through `Commentable`. + + This relation attribute behaves similar to [hasMany](#hasmany) but without + the model type constraint. + + | Arguments | | + |------------|---------------------------------------------------------------------------| + | `related`* | The target model e.g. `Comment` or `'comments'`. | + | `id`* | The attribute to hold the target model foreign key e.g. `commentable_id`. | + | `type`* | The attribute to hold the target model entity e.g. `commentable_type`. | + | `localKey` | The custom key on the target model it's foreign key points to. | + +- **See also**: [Relationships: One To Many (Polymorphic)](/guide/model/relationships.md#one-to-many-polymorphic) + +### `morphToMany` + +- **Type**: `(related: Model | string, pivot: Model | string, relatedId: string, id: string, type: string, parentKey?: string, relatedKey?: string) => Object` + + Create a many-to-many polymorphic relation where more than one type of model + may own any number of records on the target model connected by an intermediate model. + For example, an infinite number of `Tag` may belong to any amount of `User` + and `Post` records scoped through `Taggable`. + + This relation attribute behaves similar to [belongsToMany](#belongstomany) + but without the model type constraint. + + | Arguments | | + |--------------|-----------------------------------------------------------------------| + | `related`* | The source model e.g. `Tag` or `'tags'` | + | `pivot`* | The intermediate model e.g. `Taggable` | + | `relatedId`* | The attribute to hold the source model foreign key e.g. `tag_id` | + | `id`* | The attribute to hold the target model foreign key e.g. `taggable_id` | + | `type`* | The attribute to hold the target model entity e.g. `taggable_type` | + | `parentKey` | The custom key on the target model it's foreign key points to. | + | `relatedKey` | The custom key on the source model it's foreign key points to. | + +- **See also**: [Relationships: Many To Many (Polymorphic)](/guide/model/relationships.md#many-to-many-polymorphic) + +### `morphedByMany` + +- **Type**: `(related: Model | string, pivot: Model | string, relatedId: string, id: string, type: string, parentKey?: string, relatedKey?: string) => Object` + + Create a many-to-many polymorphic inverse relation where a record on the + target model belongs to more than one type of source model connected by an + intermediate model. + For example, `Tag` may belong to many `Post` and `User` scoped through `Taggable`. + + | Arguments | | + |--------------|-----------------------------------------------------------------------| + | `related`* | The target model e.g. `Post` or `'posts'` | + | `pivot`* | The intermediate model e.g. `Taggable` | + | `relatedId`* | The attribute to hold the source model foreign key e.g. `tag_id` | + | `id`* | The attribute to hold the target model foreign key e.g. `taggable_id` | + | `type`* | The attribute to hold the target model entity e.g. `taggable_type` | + | `parentKey` | The custom key on the source model it's foreign key points to. | + | `relatedKey` | The custom key on the target model it's foreign key points to. | + +- **See also**: [Relationships: Many To Many Inverse (Polymorphic)](/guide/model/relationships.md#defining-the-inverse-of-the-relationship-2) diff --git a/docs/api/model/lifecycle-hooks.md b/docs/api/model/lifecycle-hooks.md new file mode 100644 index 00000000..d4fde0d7 --- /dev/null +++ b/docs/api/model/lifecycle-hooks.md @@ -0,0 +1,105 @@ +# Lifecycle Hooks + +Vuex ORM triggers several lifecycle hooks while interacting with the store. Lifecycle hooks allow interaction with particular points in a query lifecycle, executing code each time a record is saved, updated or retrieved from the store. + +Lifecycle hooks can be configured on models as static methods. Hooks registered on models are only triggered when they interact with the store. + +There are two forms of lifecycle hooks. [Select Lifecycle Hooks](#select-hooks) and [Mutation Lifecycle Hooks](#mutation-hooks) both of which offer several hooks during store interaction. + +**Example**: + +```js +class User extends Model { + static entity = 'users' + + static fields () { + /* ... */ + } + + static beforeSelect (records) { + return users.filter(user => user.admin !== 'admin') + } + + static beforeCreate (record) { + record.published = true + } +} +``` + +::: tip +Lifecycle hooks are documented in order of execution. +::: + +**See also**: [Global Hooks](/api/query/query.md#hooks) + + +## Select Hooks + +Select lifecycle hooks are called when retrieving data from the store. They are triggered at several stages of the retrieval process. Each of these hooks receive an array of model instances that are being processed by a query. The number of instances may vary as it traverses through the selection process. + +Select lifecycle hooks **must always return a collection** of model instances. + +### `beforeSelect` + +- **Type**: `(records: Array) => Array` + + Triggered at the very beginning of the retrieving process before any query filters are applied. + +### `afterWhere` + +- **Type**: `(records: Array) => Array` + + Triggered after any [where](/api/query/query.md#where) clauses have been applied. + +### `afterOrderBy` + +- **Type**: `(records: Array) => Array` + + Triggered after any [orderBy](/api/query/query.md#orderby) filters have been applied. + +### `afterLimit` + +- **Type**: `(records: Array) => Array` + + Triggered after any [limit](/api/query/query.md#limit) and [offset](/api/query/query.md#offset) filters have been applied. + + +## Mutation Hooks + +Mutation lifecycle hooks are called when mutating data in the store. + +### `beforeCreate` + +- **Type**: `(record: Model | Array) => Model | Array | boolean` + + Triggered before records are inserted into the store. Returning `false` will prevent persisting records to the store. + +### `afterCreate` + +- **Type**: `(record: Model | Array) => void` + + Triggered after records are inserted into the store. + +### `beforeUpdate` + +- **Type**: `(record: Model | Array) => Model | Array | boolean` + + Triggered before records are updated in the store. Returning `false` will prevent persisting records to the store. + +### `afterUpdate` + +- **Type**: `(record: Model | Array) => void` + + Triggered after records are updated in the store. + +### `beforeDelete` + +- **Type**: `(record: Model | Array) => Model | Array | boolean` + + Triggered before records are deleted from the store. Returning `false` will prevent records being deleted from the store. + +### `afterDelete` + +- **Type**: `(record: Model | Array) => void` + + Triggered after records are deleted from the store. diff --git a/docs/api/model/model.md b/docs/api/model/model.md index 0f52581b..04ee769d 100644 --- a/docs/api/model/model.md +++ b/docs/api/model/model.md @@ -1,46 +1,236 @@ ---- -sidebarDepth: 2 ---- - # Model +## Static Properties + +### `entity` + +- **Type**: `string` + + The name of the module under which a model will store entity data. + +- **See also**: [Defining Models: Entity Name](/guide/model/defining-models.md#entity-name) + +### `baseEntity` + +- **Type**: `string` + + The reference to the base entity name if the model extends another model. This is usually defined when using [Single Table Inheritance](/guide/model/single-table-inheritance.md). + +### `primaryKey` + +- **Type**: `string | Array = 'id'` + + The primary key to be used for the model. This defaults to `id` and can be overridden to suit the needs of your application. + + Composite primary keys can be defined by setting this property as an array of keys. + +- **See also**: [Defining Models: Primary Key](/guide/model/defining-models.md#primary-key) + +### `typeKey` + +- **Type**: `string = 'type'` + + The discriminator key to be used for the model when using [Single Table Inheritance](/guide/model/single-table-inheritance.md). + +- **See also**: [Discriminator Field Override](/guide/model/single-table-inheritance.md#discriminator-field) + + ## Static Methods -### store +### `new` + +- **Type**: `() => Promise` + + Create new records with all declared fields filled with their default values. + + Returns a Promise that resolves with the newly created record. + +- **See also**: [Inserting & Updating](/guide/data/inserting-and-updating.md#inserting-updating) + +### `create` + +- **Type**: `(payload: Object | Array, options?: Object) => Promise` -- **`store(): void`** +- **Options**: `{ create, insert, update, insertOrUpdate }` - Get Vuex Store instance. + Create records by replacing all existing entity data in the store with the payload provided. This will flush all existing records before inserting new records. + + The `options` argument notifies Vuex ORM on how to handle any relationships. [Details](/guide/data/inserting-and-updating.md#insert-method-for-relationships). + + Returns a Promise that resolves with created records. + +- **See also**: [Inserting & Updating](/guide/data/inserting-and-updating.md#inserting-updating) + +### `insert` + +- **Type**: `(payload: Object | Array, options?: Object) => Promise` + +- **Options**: `{ create, insert, update, insertOrUpdate }` + + Insert records by adding new records and replacing any existing records that + match the primary key of the records provided in the payload. + + The `options` argument notifies Vuex ORM on how to handle any relationships. [Details](/guide/data/inserting-and-updating.md#insert-method-for-relationships). + + Returns a Promise that resolves with inserted records. + +- **See also**: [Inserting & Updating](/guide/data/inserting-and-updating.md#inserting-updating) + +### `update` + +- **Type**: `(payload: Object | Array | (model) => void, options?: Object) => Promise` + +- **Condition**: `{ where: number | string | (model) => boolean }` + +- **Options**: `{ create, insert, update, insertOrUpdate }` + + Update existing records that matches the primary key against the `where` + condition. In the absence of a condition, records will be matched + against primary keys in the payload itself. + + The `where` condition may also be a closure for pragmatic conditionals. + + ```js + User.update( + { name: 'Johnny Doe' }, + { where: (user) => user.name === 'John Doe' } + ) + ``` + + A closure may also be passed as the payload, only when accompanied by a `where` + condition, as an alternate syntax for handling record data. ```js - const store = User.store() + User.update((user) => { user.name = 'Johnny Doe' }, where: 1 }) ``` -### dispatch + Additional `options` provided notifies Vuex ORM on how to handle any relationships. [Details](/guide/data/inserting-and-updating.md#insert-method-for-relationships). + + Returns a Promise that resolves with updated records. + +- **See also**: [Inserting & Updating](/guide/data/inserting-and-updating.md#updates) + +### `insertOrUpdate` + +- **Type**: `(payload: Object | Array, options?: Object) => Promise` -- **`dispatch(method: string, payload?: any): Promise`** +- **Options**: `'create'` | `'insert'` | `'update'` | `'insertOrUpdate'` - Dispatch a store action. It will generate module namespace automatically. + Inserts new records and updates any existing. Similar to [insert](#insert) + except the payload is explored for records that require insertion and records + that already exist and need only updating. + + Two mutations are committed to the store, one of which inserts records, + and the other updates. + + The `options` argument notifies Vuex ORM on how to handle any relationships. [Details](/guide/data/inserting-and-updating.md#insert-method-for-relationships). + + Returns a Promise that resolves with inserted and/or updated records. + +- **See also**: [Inserting & Updating](/guide/data/inserting-and-updating.md#insert-or-update) + +### `delete` + +- **Type**: + - `(id: string | number | Array) => Promise` + - `(condtion: (model) => boolean): Promise` + - `(payload: any): Promise` + + Delete entity records by primary key or condition. + + ```js + // Delete by primary key + User.delete(1) + + // Delete by composite primary key + User.delete([1, 2]) + + // Delete by conditional closure + User.delete((user) => user.subscription === 'expired') + ``` + +- **See also**: [Deleting](/guide/data/deleting.md) + +### `deleteAll` + +- **Type**: `() => Promise` + + Delete all entity records from the store. + +- **See also**: [Delete All Data](/guide/data/deleting.md#delete-all-data) + +### `all` + +- **Type**: `() => Array` + + Get all entity records. + +- **See also**: [Get All Data](/guide/data/retrieving.md#get-all-data) + +### `find` + +- **Type**: `(id: string | number | Array) => Object | null` + + Find a record by primary key. + +- **See also**: [Get Single Data](/guide/data/retrieving.md#get-single-data) + +### `findIn` + +- **Type**: `(idList: Array>) => Array` + + Find records by an array of primary keys. + +- **See also**: [Get Multiple Data by Primary Keys](/guide/data/retrieving.md#get-multiple-data-by-primary-keys) + +### `query` + +- **Type**: `() => Query` + + Get a new [Query Builder](/guide/data/retrieving.md#query-builder) instance. + +### `store` + +- **Type**: `() => Vuex.Store` + + Get the Vuex Store instance. + +### `database` + +- **Type**: `() => Database` + + Get the database instance from store. + +### `dispatch` + +- **Type**: `(method: string, payload?: Object, options?: Object) => Promise` + + Dispatches a store action. The module namespace is generated automatically. ```js - User.dispatch('create', { data: { /* ... */ } }) + User.dispatch('create', { data: [...] }) ``` -### getters +- **See also**: [Call Module Methods From Model](/guide/digging-deeper/vuex-module.md#call-module-methods-from-model) + +### `getters` -- **`getters(method: string): any`** +- **Type**: `(method: string) => any` - Call a getter. It will generate module namespace automatically. + Calls a store getter. The module namespace is generated automatically. ```js - const users = User.$getters('all')() + const users = User.getters('all')() ``` -### namespace +- **See also**: [Calling Getters](/guide/digging-deeper/vuex-module.md#calling-getters) -- **`namespace(method: string): string`** +### `namespace` - Get namespaced string to be used for dispathing actions or calling getters. +- **Type**: `(method: string) => string` + + Creates a namespaced string respresentation for the given method. Useful when + dispatching actions or calling getters. ```js const method = User.namespace('create') @@ -48,11 +238,23 @@ sidebarDepth: 2 // 'entities/users/create' ``` -### hydrate +### `commit` + +- **Type**: `(callback: (state: Object) => void) => void` + + Commits a store mutation. The module namespace is generated automatically. + + ```js + User.commit((state) => { state.fetching = true }) + ``` + +- **See also**: [Mutating State](/guide/digging-deeper/vuex-module.md#mutating-state) + +### `hydrate` -- **`hydrate(record?: Record): Record`** +- **Type**: `(record?: Object) => Object` - Fill any missing fields in the given record with the default value defined in the model schema. Note that the returned object is not Model instance, it's plain object. + Fill any missing fields in the given record with the default values defined in the model schema. The return type is a plain object. ```js User.hydrate({ id: 1 }) @@ -60,7 +262,7 @@ sidebarDepth: 2 // { id: 1, name: 'Default Name' } ``` - If you pass relational data, those will be hydrated as well. + Relational data will also be hydrated if it exists. ```js User.hydrate({ @@ -83,123 +285,208 @@ sidebarDepth: 2 */ ``` - > **NOTE:** `hydrate` method will not "normalize" the given data. It will fill any missing field, but it wouldn't attach correct id value to the foreign field, for example adding `id` value of the user to the `user_id` field of the post, or increment the value specified by the `uid` attribute. - -## Instance Methods + ::: tip + Hydrating will not "normalize" the given data. It will fill any missing + fields, but it will not attach correct id value to the foreign field, for + example adding `id` value of the user to the `user_id` field of the post, or + increment the value specified by the `uid` attribute. + ::: -### $store +### `getFields` -- **`$store(): void`** +- **Type**: `() => Object` - Get Vuex Store instance. + Get the fields definition of the model and its relations. ```js - const user = new User() + class User extends Model { + static entity = 'users' - const store = user.$store() + static fields () { + return { + id: this.attr(null), + name: this.attr('John Doe') + } + } + } + + const fields = (new User()).$fields() + + /* + { + id: { + value: null, // default value + ... + }, + name: { + value: John Doe, // default value + ... + } + } + */ ``` -### $dispatch -- **`$dispatch(method: string, payload?: any): Promise`** +## Instance Properties - Dispatch a store action. It will generate module namespace automatically. +### `$id` - ```js - const user = new User() +- **Type**: `string | null` - user.$dispatch('create', { data: { /* ... */ } }) - ``` + Stores the value of the primary key. This is the generated index identifier for a + record and returns a string representation of the primary key. -### $getters +## Instance Methods -- **`$getters(method: string): any`** +### `$create` - Call a getter. It will generate module namespace automatically. +- **Type**: `(payload: Object | Array, options?: Object) => Promise` - ```js - const user = new User() +- **Options**: `{ create, insert, update, insertOrUpdate }` - const users = user.$getters('all')() - ``` + The instance method of [create()](#create). + +### `$insert` + +- **Type**: `(payload: Object | Array, options?: Object) => Promise` + +- **Options**: `{ create, insert, update, insertOrUpdate }` + + The instance method of [insert()](#insert). + +### `$update` + +- **Type**: `(payload: Object | Array | Function, options?: Object) => Promise` + +- **Condition**: `{ where: number | string | (record) => boolean }` + +- **Options**: `{ create, insert, update, insertOrUpdate }` + + The instance method of [update()](#update) with one exception: + + - Omitting a condition and primary key from the payload results in `where` + using the primary key from the instance itself. -### $namespace + ```js + const user = User.find(1) -- **`$namespace(method: string): string`** + user.$update({ name: 'Johnny Doe'}) + ``` - Get namespaced string to be used for dispathing actions or calling getters. + is equvalent to: + + ```js + User.update({ name: 'Johnny Doe' }, { where: 1 }) + ``` + +### `$insertOrUpdate` + +- **Type**: `(payload: Object | Array, options?: Object) => Promise` + +- **Options**: `'create'` | `'insert'` | `'update'` | `'insertOrUpdate'` + + The instance method of [insertOrUpdate()](#insertorupdate). + +### `$save` + +- **Type**: `() => Promise` + + Persist the current instance to the store. This method uses the `insertOrUpdate` + action internally and it is advised to use `$save()` where the model has been + instantiated using the `new` operator. ```js const user = new User() - const method = user.$namespace('create') + user.name = 'John Doe' - // 'entities/users/create' + user.$save() ``` -### $fields +- **See also**: [Save Method](/guide/data/inserting-and-updating.md#save-method) -- **`$fields(): Object`** +### `$delete` - Get the `fields` object of the model. +- **Type**: `() => Promise` - ```js - import { Model } from '@vuex-orm/core' + Delete the current instance record from the store. - class User extends Model { - static entity = 'users' +### `$deleteAll` - static fields () { - return { - id: this.attr(null), - name: this.attr('John Doe') - } - } - } +- **Type**: `() => Promise` - const user = new User() + The instance method of [deleteAll()](#deleteall). - user.$fields() +### `$all` - /* - { - username: { - value: null, // default value - ... - }, - name: { - value: John Doe, // default value - ... - } - } - */ - ``` +- **Type**: `() => Array` -### $id + The instance method of [all()](#all). -- **`$id(): any`** +### `$find` - Get the value of the primary key. +- **Type**: `(id: string | number | Array) => Object | null` - ```js - import { Model } from '@vuex-orm/core' + The instance method of [find()](#find). - class User extends Model { - static entity = 'users' +### `$findIn` - static primaryKey = 'username' +- **Type**: `(idList: Array>) => Array` - static fields () { - return { - username: this.attr(null), - name: this.attr('') - } - } - } + The instance method of [findIn()](#findin). - const user = new User({ username: 'john-doe', name: 'John Doe' }) +### `$query` - user.$id() +- **Type**: `() => Query` - // 'john-doe' - ``` + The instance method of [query()](#query). + +### `$store` + +- **Type**: `() => Vuex.Store` + + Get the Vuex Store instance. + +### `$dispatch` + +- **Type**: `(method: string, payload?: Object, options?: Object) => Promise` + + The instance method of [dispatch()](#dispatch). + +### `$getters` + +- **Type**: `(method: string) => any` + + The instance method of [getters()](#getters). + +### `$namespace` + +- **Type**: `(method: string) => string` + + The instance method of [namespace()](#namespace). + +### `$fields` + +- **Type**: `() => Object` + + The instance method of [getFields()](#getfields). + +### `$primaryKey` + +- **Type**: `() => string | Array` + + Get the primary key name for the model. Defaults to `id`. + +### `$getAttributes` + +- **Type**: `() => Object` + + Equivalent to [hydrate](#hydrate) except it is invoked on the current instance. + +### `$toJson` + +- **Type**: `() => Object` + + Serializes fields and their values into JSON. + +- **See also**: [Serialization](/guide/digging-deeper/serialization.md) diff --git a/docs/api/query/query.md b/docs/api/query/query.md new file mode 100644 index 00000000..0b4eda0c --- /dev/null +++ b/docs/api/query/query.md @@ -0,0 +1,251 @@ +# Query + +Query instances may be obtained by calling the [query](/api/model/model.md#query) method from a model. + +**See also**: [Query Builder](/guide/data/retrieving.md#query-builder) + +## Filters + +### `where` + +- **Type**: + + - `(field: string, value: any) => Query` + - `(field: string, value: (model) => boolean) => Query` + - `(field: (model, query) => void) => Query` + + Add a "where" condition. Subsequent "where" conditions are combined with the `and` operator. + + ```js + // Field equality + User.query().where('age', '20') + + // Field equality by expression + User.query().where('age', (user) => user.age > 20) + + // Expression + User.query().where((user) => user.age > 20 && user.sex === 'female') + + // Constraint groups + User.query().where('role', 'user').where((model, query) => { + query.where('age', '20').orWhere('sex', 'female') + }) + ``` + + Closures will receive two arguments - a model instance and a query instance. The query instance allows for adding constraint groups such as, but not limited to, parameter grouping. This type of closure does not require a return value. However, expressions must return a boolean. + + ::: warning NOTE + Expression closures require a boolean return type. In such cases returning truthy/falsy, such as `array.length`, `null` or `undefined`, will result in unexpected side-effects ([#402](https://github.com/vuex-orm/vuex-orm/issues/402)). This is a design limitation since constraint closures don't expect a return type. + ::: + +- **See also**: [Where Clauses](/guide/data/retrieving.md#where-clauses) + +### `orWhere` + +- **Type**: + + - `(field: string, value: any) => Query` + - `(field: string, value: (model) => boolean) => Query` + - `(field: (model, query) => void) => Query` + + Extend [where()](#where) condition with the `or` operator. + +### `whereId` + +- **Type**: `(value: number | string) => Query` + + Filter records by primary key. + +### `whereIdIn` + +- **Type**: `(values: Array) => Query` + + Filter records by an array of primary keys. + +### `offset` + +- **Type**: `(offset: number) => Query` + + Add an offset expression to skip a certain number of records before beginning to return the result. + +### `limit` + +- **Type**: `(limit: number) => Query` + + Add a limit expression to constrain the number of records to be returned. + +### `orderBy` + +- **Type**: `(field: string | (model) => any, direction: string = 'asc') => Query` + +- **Directions**: `'asc'` | `'desc'` + + Sort the result set by field. The default sort direction is `asc`. + + A closure may be passed as the first argument which is applied on each result iteration. The closure may return a value to apply as the sorting value. + + ```js + // Sort user name by its 3rd character + User.query().orderBy(user => user.name[2]) + ``` + +- **See also**: [Order By](/guide/data/retrieving.md#order-by) + + +## Relations + +By default, Vuex ORM does not query relations unless explicitly told to with the following methods. + +**See also**: [Retrieving Relationships](/guide/data/retrieving.md#relationships) + +### `with` + +- **Type**: `(name: string | Array, constraint: (query) => void | null = null) => Query` + + Eager load relations by attribute name. + + Using dot notation, it is possible to load child relations. + + ```js + Users.query().with('posts.comments') + ``` + + In addition, using a dot notation wildcard makes it possible to load all child relations of a relation. + + ```js + Users.query().with('posts.*') + ``` + + Constraints may be added by passing a closure as the second argument. The closure will receive a query instance as a single argument. + + ```js + User.query().with('posts', (query) => { + query.where('published', true) + }) + ``` + +### `withAll` + +- **Type**: `() => Query` + + Eager load all relations defined on the model. + +### `withAllRecursive` + +- **Type**: `(depth: number = 3) => Query` + + Eager load all relations recursively. By default the recursion `depth` is 3 levels. + +### `has` + +- **Type**: `(relation: string, operator?: string | number, count?: number) => Query` + + Add a conditional constraint based on relation existence. + + Where `count` is supplied as the second argument, relations will be constrained to a number of records it holds. + + Additionally, an operator can be added as the second argument in addition to the `count` as the third argument for a more concise comparison. Supported operators are: `'>'`, `'>='`, `'='`, `'<='`, `'<'`. + + ```js + // Filter posts having at least 2 comments + Post.query().has('comments', 2) + + // Filter posts having more than 2 comments + Post.query().has('comments', '>', 2) + ``` + +### `hasNot` + +- **Type**: `(relation: string, operator?: string | number, count?: number) => Query` + + Add a conditional constraint based on relation absence. See [has()](#has) for detail on usage. + +### `whereHas` + +- **Type**: `(relation: string, constraint: (query) => void) => Query` + + Add a conditional "where" constraint based on relation existence. + + ```js + Post.query().whereHas('comments', (query) => { + query.where('user_id', 1) + }) + ``` + +### `whereHasNot` + +- **Type**: `(relation: string, constraint: (query) => void) => Query` + + Add a conditional "where" constraint based on relation absence. See [whereHas()](#wherehas) for detail on usage. + + +## Results + +### `get` + +- **Type**: `() => Array` + + Execute the query chain and return the result. + +### `first` + +- **Type**: `() => Object | null` + + Execute the query chain and return the first record from the result set. + +### `last` + +- **Type**: `() => Object | null` + + Execute the query chain and return the last record from the result set. + +### `count` + +- **Type**: `() => number` + + Execute the query chain and return the total number of records in the result set. + +### `min` + +- **Type**: `(field: string) => number` + + Execute the query chain and return the [min](https://en.wikipedia.org/wiki/Maxima_and_minima) value of field. + +### `max` + +- **Type**: `(field: string) => number` + + Execute the query chain and return the [max](https://en.wikipedia.org/wiki/Maxima_and_minima) value of field. + +### `sum` + +- **Type**: `(field: string) => number` + + Execute the query chain and eturn the [sum](https://en.wikipedia.org/wiki/Summation) + value of field. + +## Lifecycle Hooks + +Registering global lifecycle hooks grants interaction with particular points in a query lifecycle on a global scale. Liefecycle hooks can also be [registered on models](../model/lifecycle-hooks.md). The number of arguments passed to a callback also depends on the type of hook being registered. + +**See also**: [Lifecycle Hooks](/guide/digging-deeper/lifecycle-hooks) + +### `on` + +- **Type**: `(on: string, callback: (model) => boolean | void) => number` + + ```js + Query.on('beforeCreate', (record) => [...]) + ``` + + Certain [lifecycle hooks](/guide/digging-deeper/lifecycle-hooks) allow a boolean to be returned to permit or prevent state mutations. + +### `off` + +- **Type**: `(id: number) => boolean` + + ```js + const hookId = Query.on('beforeCreate', (record) => [...]) + + Query.off(hookId) + ``` diff --git a/docs/guide/digging-deeper/vuex-module.md b/docs/guide/digging-deeper/vuex-module.md index aad3de51..675465ff 100644 --- a/docs/guide/digging-deeper/vuex-module.md +++ b/docs/guide/digging-deeper/vuex-module.md @@ -109,7 +109,7 @@ const count = store.state.users.count store.commit('entities/users/add', 3) ``` -## Call Module Methods fom Model +## Call Module Methods From Model An alternative to calling store methods directly, you may access the store instance from a model as well to dispatch actions or call getters. diff --git a/docs/guide/model/single-table-inheritance.md b/docs/guide/model/single-table-inheritance.md index f1fe6066..9961fbd6 100644 --- a/docs/guide/model/single-table-inheritance.md +++ b/docs/guide/model/single-table-inheritance.md @@ -73,7 +73,7 @@ const people = Person.all() /* [ Person { id: 1, name: 'John Doe' }, - Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer' } + Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer' } ] */ ``` @@ -127,19 +127,17 @@ Now, you can create mixed types of records at once. ```js // Creating mixed data. -Person.insert({ - data: [ - { type:'PERSON', id: 1, name: 'John Doe' }, - { type:'ADULT', id: 2, name: 'Jane Doe', job: 'Software Engineer' } - ] -}) +Person.insert([ + { type: 'PERSON', id: 1, name: 'John Doe' }, + { type: 'ADULT', id: 2, name: 'Jane Doe', job: 'Software Engineer' } +]) const people = Person.all() /* [ Person { id: 1, name: 'John Doe' }, - Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer' } + Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer' } ] */ ``` @@ -188,19 +186,17 @@ class Adult extends Person { And now you may use a custom `type` field when inserting data. ```js -Person.insert({ - data: [ - { person_type: 'PERSON', id: 1, name: 'John Doe' }, - { person_type: 'ADULT', id: 2, name: 'Jane Doe', job: 'Software Engineer' } - ] -}) +Person.insert([ + { person_type: 'PERSON', id: 1, name: 'John Doe' }, + { person_type: 'ADULT', id: 2, name: 'Jane Doe', job: 'Software Engineer' } +]) const people = Person.all() /* [ Person { id: 1, name: 'John Doe' }, - Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer' } + Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer' } ] */ ``` @@ -247,19 +243,17 @@ Then you can fetch the key with its results. ```js // Creating mixed data -Person.insert({ - data: [ - { type:'PERSON', id: 1, name: 'John Doe' }, - { type:'ADULT', id: 2, name: 'Jane Doe', job: 'Software Engineer' } - ] -}) +Person.insert([ + { type: 'PERSON', id: 1, name: 'John Doe' }, + { type: 'ADULT', id: 2, name: 'Jane Doe', job: 'Software Engineer' } +]) const people = Person.all() /* [ Person { id: 1, name: 'John Doe', type: 'PERSON' }, - Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer', type: 'ADULT' } + Adult { id: 2, name: 'Jane Doe', job: 'Software Engineer', type: 'ADULT' } ] */ ``` @@ -315,20 +309,24 @@ class Address extends Model { And let's see what would happen in this case. ```js -Address.insert({ - data: [ - { id: 1, city: 'TOKYO' }, - { id: 2, city: 'PARIS' }, - { id: 3, city: 'BERLIN' } - ] -}) +Address.insert([ + { id: 1, city: 'TOKYO' }, + { id: 2, city: 'PARIS' }, + { id: 3, city: 'BERLIN' } +]) Person.insert({ - data: { id: 1, home_address_id: 1, name: 'John Doe' } + id: 1, + home_address_id: 1, + name: 'John Doe' }) Adult.insert({ - data: { id: 2, home_address_id: 2, work_address_id: 3, name: 'Jane Doe', job: 'Software Engineer' } + id: 2, + home_address_id: 2, + work_address_id: 3, + name: 'Jane Doe', + job: 'Software Engineer' }) const people = Person.query().with(['home_address', 'work_address']).get() @@ -540,9 +538,7 @@ import Adult from '@/models/Adult'; export default { created () { - Adult.insert({ - data: { id: 1, name: 'John Doe', job: 'Software Engineer' } - }) + Adult.insert({ id: 1, name: 'John Doe', job: 'Software Engineer' }) } } ``` From ef878f5158fb78b9ca5c464cb8cbbb65afbe2e62 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Wed, 1 Apr 2020 16:41:15 +0100 Subject: [PATCH 04/56] chore: replace node env with __DEV__ global --- src/store/install.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/install.ts b/src/store/install.ts index 08f44981..21183936 100644 --- a/src/store/install.ts +++ b/src/store/install.ts @@ -70,7 +70,7 @@ function mixinRepoFunction(store: Store): void { function mixinDbFunction(store: Store): void { store.$db = function(): HackedDatabase { /* istanbul ignore next */ - if (process.env.NODE_ENV !== 'production') { + if (__DEV__) { console.warn( '[Vuex ORM] `store.$db()` method is deprecated. Please use ' + '`store.$repo(Model)` method instead.' From 17cd11e3c62bc1568b0efab0c0949fc6afbbf919 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Wed, 1 Apr 2020 18:39:50 +0100 Subject: [PATCH 05/56] chore: fix sponsor alignment --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 011171eb..2f131ede 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Vuex ORM is heavily inspired by Redux recipe of ["Normalizing State Shape"](http

Super Love Sponsors

-
+

Peter Tóth @@ -48,7 +48,7 @@ Vuex ORM is heavily inspired by Redux recipe of ["Normalizing State Shape"](http Dylan Copeland -

+

Big Love Sponsors

From 803462eae786278d4539d5980b7511f1acf487d2 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Wed, 1 Apr 2020 19:59:22 +0100 Subject: [PATCH 06/56] chore: remove code standard badge --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2f131ede..26970202 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,15 @@

Vuex ORM

+ + npm + Travis CI codecov - - npm - - - JavaScript Style Guide - License From aa9de263b57ca996fee598e9c58b9aaf7f9c7699 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Wed, 1 Apr 2020 22:05:05 +0100 Subject: [PATCH 07/56] docs: replace Standard JS with Prettier --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26970202..d8fe6503 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Compile files and generate bundles in `dist` directory. $ yarn lint ``` -Lint files using a rule of Standard JS. +Lint files using [Prettier](https://prettier.io/). ```bash $ yarn test From ca64d97be89a38dcd93812b2b894ad1740a91d68 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Sat, 4 Apr 2020 16:26:26 +0900 Subject: [PATCH 08/56] style(polyfills): clean the file a bit --- src/polyfills/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/polyfills/index.ts b/src/polyfills/index.ts index ff16b43f..b505cb3d 100644 --- a/src/polyfills/index.ts +++ b/src/polyfills/index.ts @@ -1,8 +1,6 @@ -/*eslint-disable */ - +import 'core-js/fn/array/from' import 'core-js/fn/array/includes' import 'core-js/fn/object/assign' import 'core-js/fn/object/entries' import 'core-js/fn/object/values' import 'core-js/fn/string/starts-with' -import 'core-js/fn/array/from' From 2270a8a36c4c829459ec6dbf5d2f9a12d26e67bf Mon Sep 17 00:00:00 2001 From: Kia King Ishii Date: Sat, 4 Apr 2020 17:32:33 +0900 Subject: [PATCH 09/56] refactor: installation related structures (#615) --- src/index.cjs.ts | 47 +-------------- src/index.ts | 45 +-------------- src/plugins/use.ts | 2 +- src/store/{install.ts => index.ts} | 8 +-- test/feature/VuexORM.spec.ts | 86 ---------------------------- test/feature/modules/modules.spec.ts | 48 ++++++++++++++++ test/unit/VuexORM.spec.ts | 76 ++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 178 deletions(-) rename src/store/{install.ts => index.ts} (92%) delete mode 100644 test/feature/VuexORM.spec.ts create mode 100644 test/feature/modules/modules.spec.ts create mode 100644 test/unit/VuexORM.spec.ts diff --git a/src/index.cjs.ts b/src/index.cjs.ts index 32496d6f..66ebfe54 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,11 +1,10 @@ import './polyfills' -import install, { Install } from './store/install' -import use, { Use } from './plugins/use' +import { install } from './store' +import { use } from './plugins/use' import Container from './container/Container' import Database from './database/Database' import Model from './model/Model' -import Fields from './model/contracts/Fields' import Query from './query/Query' import Attribute from './attributes/Attribute' import Type from './attributes/types/Type' @@ -32,46 +31,6 @@ import Actions from './modules/Actions' import RootGetters from './modules/RootGetters' import RootActions from './modules/RootActions' import RootMutations from './modules/RootMutations' -import GettersContract from './modules/contracts/Getters' -import ActionsContract from './modules/contracts/Actions' -import RootGettersContract from './modules/contracts/RootGetters' -import RootActionsContract from './modules/contracts/RootActions' -import RootMutationsContract from './modules/contracts/RootMutations' - -export interface VuexORM { - install: Install - use: Use - Container: typeof Container - Database: typeof Database - Model: typeof Model - Fields: Fields - Query: typeof Query - Attribute: typeof Attribute - Type: typeof Type - Attr: typeof Attr - String: typeof String - Number: typeof Number - Boolean: typeof Boolean - Uid: typeof Uid - Relation: typeof Relation - HasOne: typeof HasOne - BelongsTo: typeof BelongsTo - HasMany: typeof HasMany - HasManyBy: typeof HasManyBy - BelongsToMany: typeof BelongsToMany - HasManyThrough: typeof HasManyThrough - MorphTo: typeof MorphTo - MorphOne: typeof MorphOne - MorphMany: typeof MorphMany - MorphToMany: typeof MorphToMany - MorphedByMany: typeof MorphedByMany - Repository: typeof Repository - Getters: GettersContract - Actions: ActionsContract - RootGetters: RootGettersContract - RootActions: RootActionsContract - RootMutations: RootMutationsContract -} export default { install, @@ -105,4 +64,4 @@ export default { RootActions, RootMutations, Query -} as VuexORM +} diff --git a/src/index.ts b/src/index.ts index ead67a3e..953d7d8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import './types/vuex' import './polyfills' -import install, { Install } from './store/install' -import use, { Use, PluginComponents } from './plugins/use' +import { install } from './store' +import { use, PluginComponents } from './plugins/use' import Container from './container/Container' import Database from './database/Database' import Model from './model/Model' @@ -32,11 +32,6 @@ import Actions from './modules/Actions' import RootGetters from './modules/RootGetters' import RootActions from './modules/RootActions' import RootMutations from './modules/RootMutations' -import GettersContract from './modules/contracts/Getters' -import ActionsContract from './modules/contracts/Actions' -import RootGettersContract from './modules/contracts/RootGetters' -import RootActionsContract from './modules/contracts/RootActions' -import RootMutationsContract from './modules/contracts/RootMutations' import Query from './query/Query' import Record from './data/Record' import Records from './data/Records' @@ -49,40 +44,6 @@ import Item from './data/Item' import Collection from './data/Collection' import Collections from './data/Collections' -export interface VuexORM { - install: Install - use: Use - Container: typeof Container - Database: typeof Database - Model: typeof Model - Attribute: typeof Attribute - Type: typeof Type - Attr: typeof Attr - String: typeof String - Number: typeof Number - Boolean: typeof Boolean - Uid: typeof Uid - Relation: typeof Relation - HasOne: typeof HasOne - BelongsTo: typeof BelongsTo - HasMany: typeof HasMany - HasManyBy: typeof HasManyBy - BelongsToMany: typeof BelongsToMany - HasManyThrough: typeof HasManyThrough - MorphTo: typeof MorphTo - MorphOne: typeof MorphOne - MorphMany: typeof MorphMany - MorphToMany: typeof MorphToMany - MorphedByMany: typeof MorphedByMany - Repository: typeof Repository - Getters: GettersContract - Actions: ActionsContract - RootGetters: RootGettersContract - RootActions: RootActionsContract - RootMutations: RootMutationsContract - Query: typeof Query -} - export { install, use, @@ -161,4 +122,4 @@ export default { RootActions, RootMutations, Query -} as VuexORM +} diff --git a/src/plugins/use.ts b/src/plugins/use.ts index 222eb921..f8bbef26 100644 --- a/src/plugins/use.ts +++ b/src/plugins/use.ts @@ -73,7 +73,7 @@ export interface Plugin { export type Use = (plugin: Plugin, options?: Options) => void -export default function(plugin: Plugin, options: Options = {}): void { +export function use(plugin: Plugin, options: Options = {}): void { const components: PluginComponents = { Repository, Model, diff --git a/src/store/install.ts b/src/store/index.ts similarity index 92% rename from src/store/install.ts rename to src/store/index.ts index 21183936..552b056f 100644 --- a/src/store/install.ts +++ b/src/store/index.ts @@ -4,8 +4,6 @@ import Database from '../database/Database' import HackedDatabase from '../database/HackedDatabase' import Repository from '../repository/Repository' -export type Install = (database: Database, options?: Options) => Plugin - export interface Options { namespace?: string } @@ -13,7 +11,7 @@ export interface Options { /** * Install Vuex ORM database to the store. */ -export default function install( +export function install( database: Database, options: Options = {} ): Plugin { @@ -55,8 +53,8 @@ function mixinRepoFunction(store: Store): void { throw new Error( '[Vuex ORM] The repository was instantiated without a model being ' + 'set. It happens when you forgot to register the model to the ' + - 'custom repository. Please check if you have correctly set `model` ' + - 'property at the repository class.' + 'custom repository. Please check if you have correctly set ' + + '`model` property at the repository class.' ) } diff --git a/test/feature/VuexORM.spec.ts b/test/feature/VuexORM.spec.ts deleted file mode 100644 index f3763cc8..00000000 --- a/test/feature/VuexORM.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Vue from 'vue' -import Vuex from 'vuex' -import VuexORM from '@/index' - -describe('Feature – Vuex ORM', () => { - Vue.use(Vuex) - - class User extends VuexORM.Model { - static entity = 'users' - } - - class Post extends VuexORM.Model { - static entity = 'posts' - } - - const users = {} - const posts = {} - - it('can install Vuex ORM to the Vuex', () => { - const database = new VuexORM.Database() - - database.register(User, users) - database.register(Post, posts) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(store.state.entities.$name).toBe('entities') - expect(store.state.entities.users.$name).toBe('users') - expect(store.state.entities.posts.$name).toBe('posts') - }) - - it('can omit modules', () => { - const database = new VuexORM.Database() - - database.register(User) - database.register(Post) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(store.state.entities.$name).toBe('entities') - expect(store.state.entities.users.$name).toBe('users') - expect(store.state.entities.posts.$name).toBe('posts') - }) - - it('can install Vuex ORM to the Vuex under a custom namespace', () => { - const database = new VuexORM.Database() - - database.register(User, users) - database.register(Post, posts) - - const options = { namespace: 'my_entities' } - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database, options)] - }) - - expect(store.state.my_entities.$name).toBe('my_entities') - expect(store.state.my_entities.users.$name).toBe('users') - expect(store.state.my_entities.posts.$name).toBe('posts') - }) - - it('can install Vuex ORM along with custom state as a function', () => { - const database = new VuexORM.Database() - - const users = { - state() { - return { - customState: 'Yes, it is custom' - } - } - } - - database.register(User, users) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(store.state.entities.users.$name).toBe('users') - expect(store.state.entities.users.customState).toBe('Yes, it is custom') - }) -}) diff --git a/test/feature/modules/modules.spec.ts b/test/feature/modules/modules.spec.ts new file mode 100644 index 00000000..85607d9d --- /dev/null +++ b/test/feature/modules/modules.spec.ts @@ -0,0 +1,48 @@ +import Vue from 'vue' +import Vuex, { Module } from 'vuex' +import VuexORM, { Database, Model } from '@/index' + +describe('feature/modules/modules', () => { + Vue.use(Vuex) + + it('can install a custom module along with the model', async () => { + class User extends Model { + static entity = 'users' + } + + const users: Module<{ count: number }, any> = { + state: () => ({ + count: 1 + }), + getters: { + double: (state) => state.count * 2 + }, + actions: { + increment({ commit }) { + commit('increment') + } + }, + mutations: { + increment(state) { + state.count++ + } + } + } + + const database = new Database() + + database.register(User, users) + + const store = new Vuex.Store({ + plugins: [VuexORM.install(database)] + }) + + expect(store.state.entities.users.count).toBe(1) + expect(store.getters['entities/users/double']).toBe(2) + + await store.dispatch('entities/users/increment') + + expect(store.state.entities.users.count).toBe(2) + expect(store.getters['entities/users/double']).toBe(4) + }) +}) diff --git a/test/unit/VuexORM.spec.ts b/test/unit/VuexORM.spec.ts new file mode 100644 index 00000000..9df31009 --- /dev/null +++ b/test/unit/VuexORM.spec.ts @@ -0,0 +1,76 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import VuexORM from '@/index' +import Database from '@/database/Database' +import Model from '@/model/Model' + +describe('unit/VuexORM', () => { + Vue.use(Vuex) + + it('installs Vuex ORM to the store', () => { + class User extends Model { + static entity = 'users' + } + class Post extends Model { + static entity = 'posts' + } + + const database = new Database() + + database.register(User) + database.register(Post) + + const store = new Vuex.Store({ + plugins: [VuexORM.install(database)] + }) + + const expected = { + entities: { + $name: 'entities', + users: { + $name: 'users', + $connection: 'entities', + data: {} + }, + posts: { + $name: 'posts', + $connection: 'entities', + data: {} + } + } + } + + expect(store.state).toEqual(expected) + }) + + it('can customize the namespace', () => { + class User extends Model { + static entity = 'users' + } + + const database = new Database() + + database.register(User) + + const store = new Vuex.Store({ + plugins: [ + VuexORM.install(database, { + namespace: 'database' + }) + ] + }) + + const expected = { + database: { + $name: 'database', + users: { + $name: 'users', + $connection: 'database', + data: {} + } + } + } + + expect(store.state).toEqual(expected) + }) +}) From 5f0c7db0c4031a080a2b328d32b6db47b06025f1 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Sun, 5 Apr 2020 00:22:49 +0100 Subject: [PATCH 10/56] feat(Attribute): add `hasOneThrough` relation type (#614) --- docs/guide/data/inserting-and-updating.md | 47 +- docs/guide/model/relationships.md | 66 ++- src/attributes/index.ts | 2 + src/attributes/relations/HasOneThrough.ts | 151 +++++ src/index.cjs.ts | 2 + src/index.ts | 3 + src/model/Model.ts | 22 + src/plugins/use.ts | 3 + test/feature/relations/HasOneThrough.spec.ts | 551 ++++++++++++++++++ .../Retrieve_HasOneThrough.spec.ts | 76 +++ 10 files changed, 885 insertions(+), 38 deletions(-) create mode 100644 src/attributes/relations/HasOneThrough.ts create mode 100644 test/feature/relations/HasOneThrough.spec.ts create mode 100644 test/performance/Retrieve_HasOneThrough.spec.ts diff --git a/docs/guide/data/inserting-and-updating.md b/docs/guide/data/inserting-and-updating.md index b043b4a7..8938f576 100644 --- a/docs/guide/data/inserting-and-updating.md +++ b/docs/guide/data/inserting-and-updating.md @@ -595,7 +595,11 @@ User.insert({ } ``` -However, note that there is a caveat with "Has Many Through" relationship. When creating data that contains "Has Many Through" relationship without intermediate pivot records, the intermediate record will not be generated. Let's say you have the following model definitions. +### Generating Intermediate Records + +When creating data that contains [Has One Through](../model/relationships.md#has-one-through) or [Has Many Through](../model/relationships.md#has-many-through) relations, intermediate records will not be generated without specifying supporting relations that allows Vuex ORM to normalize the data before persisting to the store. + +For example, let's take a look at the following `hasManyThrough` relation. The `Country` model can access many `Post` records through a `User` with the support of `hasMany` relations defined on `Country` and `User`. ```js class Country extends Model { @@ -634,42 +638,7 @@ class Post extends Model { } ``` -And then you try to save the following data. - -```js -Country.create({ - data: { - id: 1, - posts: [ - { id: 1 }, - { id: 2 } - ] - } -}) -``` - -Vuex ORM will normalize the data and save them to the store as below. - -```js -{ - countries: { - data: { - 1: { id: 1 } - } - }, - users: { - data: {} - }, - posts: { - data: { - 1: { id: 1, user_id: null }, - 2: { id: 2, user_id: null } - } - } -} -``` - -See there is no users record, and `user_id` at `posts` becomes empty. This happens because Vuex ORM wouldn't have any idea how post data relate to the intermediate User. Hence if you create data like this, you wouldn't be able to retrieve them by getters anymore. In such cases, it is recommended to create data with the intermediate records. +By adding the `hasMany` relation attribute on both `Country` and `User`, the following data structure can generate records for all the models required for the `hasManyThrough` relation. ```js Country.create({ @@ -693,6 +662,10 @@ Country.create({ }) ``` +The `users` property will populate `User` model through the `users` attribute defined on the `Country` model. Subsequently, nesting `posts` within a user record will populate the `Post` model through the `posts` attribute defined on the `User` model, thus creating `Post` records. This is all made possible by strategically placing the relevant relations as demonstrated. + +The same principle applies to the `hasOneThrough` relation, since it is a 1:1 version of `hasManyThrough`, by replacing `hasMany` with `hasOne` relations. + ### Get Newly Inserted Data Both `insert` and `create` return the inserted data as a Promise so that you can get them as a return value. The return value will contain all of the data that was created. diff --git a/docs/guide/model/relationships.md b/docs/guide/model/relationships.md index a644619b..e686cd6b 100644 --- a/docs/guide/model/relationships.md +++ b/docs/guide/model/relationships.md @@ -434,6 +434,66 @@ user.podcasts.forEach((podcast) => { }) ``` +## Has One Through + +The "has-one-through" relationship links models through a single intermediate relation. For example, if each `User` has one `Profile`, and each `Profile` is associated with one `Image`, then the `User` may access `Image` through `Profile`. Let's look at the models required to define this relationship: + +```js +class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } +} + +class Profile extends Model { + static entity = 'profiles' + + static fields() { + return { + id: this.attr(null), + user_id: this.attr(null) + } + } +} + +class Image extends Model { + static entity = 'images' + + static fields() { + return { + id: this.attr(null), + profile_id: this.attr(null) + } + } +} +``` + +Though `Image` does not contain a `user_id` attribute, the `hasOneThrough` relation can provide access to the user's profile image to the `User` model. Vuex ORM can accomplish this by inspecting the `user_id` on the intermediary model, in this case `Profile`, which then goes on to matching it with `Image` models it is associated with. + +The `hasOneThrough` relation attribute accepts the following arguments: + +```js +this.hasOneThrough( + Image, // The final model we wish to access. + Profile, // The intermediate "through" model. + 'user_id', // Foreign key on the intermediate "through" model. + 'profile_id', // Foreign key on the final model. + 'local_user_id', // Optional: custom key on the intermediate "through" model. + 'local_profile_id' // Optional: custom key on the final model. +) +``` + +::: tip +When creating data for models containing a `hasOneThrough` relation, you will need to define supporting relations in order to generate intermediate records. + +**Further reading**: [Generating Intermediate Records](../data/inserting-and-updating.html#generating-intermediate-records) +::: + ## Has Many Through The "has-many-through" relationship provides a convenient shortcut for accessing distant relations via an intermediate relation. For example, a Country might have many Posts through an intermediate User. In this example, you could easily gather all posts for a given country. Let's look at the models required to define this relationship: @@ -490,7 +550,11 @@ this.hasManyThrough( ) ``` -> **NOTE:** When creating data that contains `hasManyThrough` relationship without intermediate relation, the intermediate record will not be generated. [See here](../data/inserting-and-updating.html#generating-pivot-records) for more details. +::: tip +When creating data for models containing a `hasManyThrough` relation, you will need to define supporting relations in order to generate intermediate records. + +**Further reading**: [Generating Intermediate Records](../data/inserting-and-updating.html#generating-intermediate-records) +::: ## One To One (Polymorphic) diff --git a/src/attributes/index.ts b/src/attributes/index.ts index 0bd7ed89..fc70103d 100644 --- a/src/attributes/index.ts +++ b/src/attributes/index.ts @@ -10,6 +10,7 @@ import HasOne from './relations/HasOne' import BelongsTo from './relations/BelongsTo' import HasMany from './relations/HasMany' import HasManyBy from './relations/HasManyBy' +import HasOneThrough from './relations/HasOneThrough' import HasManyThrough from './relations/HasManyThrough' import BelongsToMany from './relations/BelongsToMany' import MorphTo from './relations/MorphTo' @@ -31,6 +32,7 @@ export { BelongsTo, HasMany, HasManyBy, + HasOneThrough, HasManyThrough, BelongsToMany, MorphTo, diff --git a/src/attributes/relations/HasOneThrough.ts b/src/attributes/relations/HasOneThrough.ts new file mode 100644 index 00000000..17547306 --- /dev/null +++ b/src/attributes/relations/HasOneThrough.ts @@ -0,0 +1,151 @@ +import { Schema as NormalizrSchema } from 'normalizr' +import Schema from '../../schema/Schema' +import { Record, Records, NormalizedData, Collection } from '../../data' +import Model from '../../model/Model' +import Query from '../../query/Query' +import Constraint from '../../query/contracts/RelationshipConstraint' +import Relation from './Relation' + +export type Entity = typeof Model | string + +export default class HasOneThrough extends Relation { + /** + * The related model. + */ + related: typeof Model + + /** + * The "through" parent model. + */ + through: typeof Model + + /** + * The near key on the relation. + */ + firstKey: string + + /** + * The far key on the relation. + */ + secondKey: string + + /** + * The local key on the relation. + */ + localKey: string + + /** + * The local key on the intermediary model. + */ + secondLocalKey: string + + /** + * Create a new relation instance. + */ + constructor( + model: typeof Model, + related: Entity, + through: Entity, + firstKey: string, + secondKey: string, + localKey: string, + secondLocalKey: string + ) { + super(model) /* istanbul ignore next */ + + this.related = this.model.relation(related) + this.through = this.model.relation(through) + this.firstKey = firstKey + this.secondKey = secondKey + this.localKey = localKey + this.secondLocalKey = secondLocalKey + } + + /** + * Define the normalizr schema for the relation. + */ + define(schema: Schema): NormalizrSchema { + return schema.one(this.related) + } + + /** + * Since the relation doesn't have a foreign key, there is no relational key + * to attach to the given data. + */ + attach(_key: any, _record: Record, _data: NormalizedData): void { + return + } + + /** + * Convert given value to the appropriate value for the attribute. + */ + make(value: any, _parent: Record, _key: string): Model | null { + return this.makeOneRelation(value, this.related) + } + + /** + * Load the relation for the given collection. + */ + load( + query: Query, + collection: Collection, + name: string, + constraints: Constraint[] + ): void { + const relatedQuery = this.getRelation( + query, + this.related.entity, + constraints + ) + + const throughQuery = query.newQuery(this.through.entity) + + this.addEagerConstraintForThrough(throughQuery, collection) + + const through = throughQuery.get() + + this.addEagerConstraintForRelated(relatedQuery, through) + + const related = this.mapThroughRelations(through, relatedQuery) + + collection.forEach((item) => { + const relation = related[item[this.localKey]] + + item[name] = relation || null + }) + } + + /** + * Set the constraints for the "through" relation. + */ + addEagerConstraintForThrough(query: Query, collection: Collection): void { + query.where(this.firstKey, this.getKeys(collection, this.localKey)) + } + + /** + * Set the constraints for the "related" relation. + */ + addEagerConstraintForRelated(query: Query, collection: Collection): void { + query.where(this.secondKey, this.getKeys(collection, this.secondLocalKey)) + } + + /** + * Create a new indexed map for the "through" relation. + */ + mapThroughRelations(through: Collection, relatedQuery: Query): Records { + const relations = this.mapSingleRelations( + relatedQuery.get(), + this.secondKey + ) + + return through.reduce((records, record) => { + const id = record[this.firstKey] + + const related = relations.get(record[this.secondLocalKey]) + + records[id] = related || null + + return records + }, {}) + } +} diff --git a/src/index.cjs.ts b/src/index.cjs.ts index 66ebfe54..c0159d6c 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -19,6 +19,7 @@ import BelongsTo from './attributes/relations/BelongsTo' import HasMany from './attributes/relations/HasMany' import HasManyBy from './attributes/relations/HasManyBy' import BelongsToMany from './attributes/relations/BelongsToMany' +import HasOneThrough from './attributes/relations/HasOneThrough' import HasManyThrough from './attributes/relations/HasManyThrough' import MorphTo from './attributes/relations/MorphTo' import MorphOne from './attributes/relations/MorphOne' @@ -51,6 +52,7 @@ export default { HasMany, HasManyBy, BelongsToMany, + HasOneThrough, HasManyThrough, MorphTo, MorphOne, diff --git a/src/index.ts b/src/index.ts index 953d7d8a..bcc101f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import BelongsTo from './attributes/relations/BelongsTo' import HasMany from './attributes/relations/HasMany' import HasManyBy from './attributes/relations/HasManyBy' import BelongsToMany from './attributes/relations/BelongsToMany' +import HasOneThrough from './attributes/relations/HasOneThrough' import HasManyThrough from './attributes/relations/HasManyThrough' import MorphTo from './attributes/relations/MorphTo' import MorphOne from './attributes/relations/MorphOne' @@ -65,6 +66,7 @@ export { HasMany, HasManyBy, BelongsToMany, + HasOneThrough, HasManyThrough, MorphTo, MorphOne, @@ -109,6 +111,7 @@ export default { HasMany, HasManyBy, BelongsToMany, + HasOneThrough, HasManyThrough, MorphTo, MorphOne, diff --git a/src/model/Model.ts b/src/model/Model.ts index c343c3d2..0385ebfb 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -195,6 +195,28 @@ export default class Model { ) } + /** + * Create a has one through relationship. + */ + static hasOneThrough( + related: typeof Model | string, + through: typeof Model | string, + firstKey: string, + secondKey: string, + localKey?: string, + secondLocalKey?: string + ): Attributes.HasOneThrough { + return new Attributes.HasOneThrough( + this, + related, + through, + firstKey, + secondKey, + this.localKey(localKey), + this.relation(through).localKey(secondLocalKey) + ) + } + /** * Create a has many through relationship. */ diff --git a/src/plugins/use.ts b/src/plugins/use.ts index f8bbef26..8058c976 100644 --- a/src/plugins/use.ts +++ b/src/plugins/use.ts @@ -13,6 +13,7 @@ import BelongsTo from '../attributes/relations/BelongsTo' import HasMany from '../attributes/relations/HasMany' import HasManyBy from '../attributes/relations/HasManyBy' import BelongsToMany from '../attributes/relations/BelongsToMany' +import HasOneThrough from '../attributes/relations/HasOneThrough' import HasManyThrough from '../attributes/relations/HasManyThrough' import MorphTo from '../attributes/relations/MorphTo' import MorphOne from '../attributes/relations/MorphOne' @@ -48,6 +49,7 @@ export interface PluginComponents { HasMany: typeof HasMany HasManyBy: typeof HasManyBy BelongsToMany: typeof BelongsToMany + HasOneThrough: typeof HasOneThrough HasManyThrough: typeof HasManyThrough MorphTo: typeof MorphTo MorphOne: typeof MorphOne @@ -90,6 +92,7 @@ export function use(plugin: Plugin, options: Options = {}): void { HasMany, HasManyBy, BelongsToMany, + HasOneThrough, HasManyThrough, MorphTo, MorphOne, diff --git a/test/feature/relations/HasOneThrough.spec.ts b/test/feature/relations/HasOneThrough.spec.ts new file mode 100644 index 00000000..89831179 --- /dev/null +++ b/test/feature/relations/HasOneThrough.spec.ts @@ -0,0 +1,551 @@ +import { createStore, createState } from 'test/support/Helpers' +import Model from '@/model/Model' + +describe('Features – Relations – Has One Through', () => { + it('can create entities containing the relation', async () => { + class User extends Model { + static entity = 'users' + + // @Attribute() + id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') + image!: Image + + static fields() { + return { + id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + // @Attribute() + id!: number + + // @Attribute() + user_id!: number + + static fields() { + return { + id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + // @Attribute() + id!: number + + // @Attribute() + profile_id!: number + + static fields() { + return { + id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + const store = createStore([ + { model: User }, + { model: Profile }, + { model: Image } + ]) + + await User.insert({ + id: 1, + image: { id: 1 } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, image: null } + }, + images: { + 1: { $id: '1', id: 1, profile_id: null } + }, + profiles: {} + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can resolve the relation', async () => { + class User extends Model { + static entity = 'users' + + // @Attribute() + id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') + image!: Image + + static fields() { + return { + id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + // @Attribute() + id!: number + + // @Attribute() + user_id!: number + + static fields() { + return { + id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + // @Attribute() + id!: number + + // @Attribute() + profile_id!: number + + static fields() { + return { + id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + createStore([{ model: User }, { model: Profile }, { model: Image }]) + + await User.insert([{ id: 1 }, { id: 2 }]) + + await Profile.insert([ + { id: 3, user_id: 1 }, + { id: 4, user_id: 2 } + ]) + + await Image.insert([ + { id: 5, profile_id: 3 }, + { id: 6, profile_id: 4 } + ]) + + const user = User.query() + .with('image') + .find(1) as User + + expect(user).toBeInstanceOf(User) + expect(user.id).toBe(1) + expect(user.image).toBeInstanceOf(Image) + expect(user.image.profile_id).toBe(3) + expect(user.image.id).toBe(5) + }) + + it('can resolve the relation with string value id', async () => { + class User extends Model { + static entity = 'users' + + // @Str('') + id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') + image!: Image + + static fields() { + return { + id: this.string(''), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + // @Str('') + id!: number + + // @Str('') + user_id!: number + + static fields() { + return { + id: this.string(''), + user_id: this.string('') + } + } + } + + class Image extends Model { + static entity = 'images' + + // @Str('') + id!: number + + // @Str('') + profile_id!: number + + static fields() { + return { + id: this.string(''), + profile_id: this.string('') + } + } + } + + createStore([{ model: User }, { model: Profile }, { model: Image }]) + + await User.insert([{ id: 'string-id-1' }, { id: 'string-id-2' }]) + + await Profile.insert([ + { id: 'string-id-3', user_id: 'string-id-1' }, + { id: 'string-id-4', user_id: 'string-id-2' } + ]) + + await Image.insert([ + { id: 'string-id-5', profile_id: 'string-id-3' }, + { id: 'string-id-6', profile_id: 'string-id-4' } + ]) + + const user = User.query() + .with('image') + .find('string-id-1') as User + + expect(user).toBeInstanceOf(User) + expect(user.id).toBe('string-id-1') + expect(user.image).toBeInstanceOf(Image) + expect(user.image.profile_id).toBe('string-id-3') + expect(user.image.id).toBe('string-id-5') + }) + + it('can resolve the relation when the intermediate entity is empty', async () => { + class User extends Model { + static entity = 'users' + + // @Attribute() + id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') + image!: Image + + static fields() { + return { + id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + // @Attribute() + id!: number + + // @Attribute() + user_id!: number + + static fields() { + return { + id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + // @Attribute() + id!: number + + // @Attribute() + profile_id!: number + + static fields() { + return { + id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + createStore([{ model: User }, { model: Profile }, { model: Image }]) + + await User.insert([{ id: 1 }]) + + const user = User.query() + .with('image') + .find(1) as User + + expect(user).toBeInstanceOf(User) + expect(user.id).toBe(1) + expect(user.image).toBeNull() + }) + + it('can resolve the relation when the target entity is empty', async () => { + class User extends Model { + static entity = 'users' + + // @Attribute() + id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') + image!: Image + + static fields() { + return { + id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + // @Attribute() + id!: number + + // @Attribute() + user_id!: number + + static fields() { + return { + id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + // @Attribute() + id!: number + + // @Attribute() + profile_id!: number + + static fields() { + return { + id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + createStore([{ model: User }, { model: Profile }, { model: Image }]) + + await User.insert({ id: 1 }) + + await Profile.insert({ + id: 1, + user_id: 1 + }) + + const user = User.query() + .with('image') + .find(1) as User + + expect(user).toBeInstanceOf(User) + expect(user.id).toBe(1) + expect(user.image).toBeNull() + }) + + it('can resolve the relation with custom primary keys', async () => { + class User extends Model { + static entity = 'users' + + static primaryKey = 'u_id' + + // @Attribute() + u_id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') + image!: Image + + static fields() { + return { + u_id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + static primaryKey = 'p_id' + + // @Attribute() + p_id!: number + + // @Attribute() + user_id!: number + + static fields() { + return { + p_id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + static primaryKey = 'i_id' + + // @Attribute() + i_id!: number + + // @Attribute() + profile_id!: number + + static fields() { + return { + i_id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + createStore([{ model: User }, { model: Profile }, { model: Image }]) + + await User.insert([{ u_id: 1 }, { u_id: 2 }]) + + await Profile.insert([ + { p_id: 3, user_id: 1 }, + { p_id: 4, user_id: 2 } + ]) + + await Image.insert([ + { i_id: 5, profile_id: 3 }, + { i_id: 6, profile_id: 4 } + ]) + + const user = User.query() + .with('image') + .find(1) as User + + expect(user).toBeInstanceOf(User) + expect(user.u_id).toBe(1) + expect(user.image).toBeInstanceOf(Image) + expect(user.image.profile_id).toBe(3) + expect(user.image.i_id).toBe(5) + }) + + it('can resolve the relation with custom local keys', async () => { + class User extends Model { + static entity = 'users' + + // @Attribute() + id!: number + + // @Attribute() + u_id!: number + + // @HasOneThrough(Image, Profile, 'user_id', 'profile_id', 'u_id', 'p_id') + image!: Image + + static fields() { + return { + id: this.attr(null), + u_id: this.attr(null), + image: this.hasOneThrough( + Image, + Profile, + 'user_id', + 'profile_id', + 'u_id', + 'p_id' + ) + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + // @Attribute() + id!: number + + // @Attribute() + p_id!: number + + // @Attribute() + user_id!: number + + static fields() { + return { + id: this.attr(null), + p_id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + // @Attribute() + id!: number + + // @Attribute() + i_id!: number + + // @Attribute() + profile_id!: number + + static fields() { + return { + id: this.attr(null), + i_id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + createStore([{ model: User }, { model: Profile }, { model: Image }]) + + await User.insert([ + { id: 11, u_id: 1 }, + { id: 12, u_id: 2 } + ]) + + await Profile.insert([ + { id: 13, p_id: 3, user_id: 1 }, + { id: 14, p_id: 4, user_id: 2 } + ]) + + await Image.insert([ + { id: 15, i_id: 5, profile_id: 3 }, + { id: 16, i_id: 6, profile_id: 4 } + ]) + + const user = User.query() + .with('image') + .find(11) as User + + expect(user).toBeInstanceOf(User) + expect(user.id).toBe(11) + expect(user.image).toBeInstanceOf(Image) + expect(user.image.profile_id).toBe(3) + expect(user.image.id).toBe(15) + }) +}) diff --git a/test/performance/Retrieve_HasOneThrough.spec.ts b/test/performance/Retrieve_HasOneThrough.spec.ts new file mode 100644 index 00000000..22879cad --- /dev/null +++ b/test/performance/Retrieve_HasOneThrough.spec.ts @@ -0,0 +1,76 @@ +import { createStore } from 'test/support/Helpers' +import Model from '@/model/Model' + +describe('Performance – Retrieve – Has One Through', () => { + it('should retrieve relation in time', async () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') + } + } + } + + class Profile extends Model { + static entity = 'profiles' + + static fields() { + return { + id: this.attr(null), + user_id: this.attr(null) + } + } + } + + class Image extends Model { + static entity = 'images' + + static fields() { + return { + id: this.attr(null), + profile_id: this.attr(null) + } + } + } + + const users = [] + const profiles = [] + const images = [] + + for (let i = 1; i <= 1000; i++) { + users.push({ id: i }) + } + + for (let i = 1; i <= 1000; i++) { + profiles.push({ id: i, user_id: i }) + } + + for (let i = 1; i <= 1000; i++) { + images.push({ id: i, profile_id: i }) + } + + const store = createStore([ + { model: User }, + { model: Profile }, + { model: Image } + ]) + + await store.dispatch('entities/users/create', { data: users }) + await store.dispatch('entities/profiles/create', { data: profiles }) + await store.dispatch('entities/images/create', { data: images }) + + const start = +new Date() + + store.getters['entities/users/query']() + .with('image') + .get() + + const end = +new Date() + + expect(end - start).toBeLessThan(300) + console.info('\x1b[2m%s\x1b[0m', ` -- The test took ${end - start}ms`) + }) +}) From aeac77fbf2e283c7839d21771ddc83a37449061e Mon Sep 17 00:00:00 2001 From: Kia King Ishii Date: Sun, 5 Apr 2020 14:26:22 +0900 Subject: [PATCH 11/56] test: add inserts new tests (#616) --- src/model/Model.ts | 2 +- test/feature/basics/New.spec.ts | 46 ----------------- .../feature/models/static_inserts_new.spec.ts | 50 +++++++++++++++++++ test/feature/modules/api_inserts_new.spec.ts | 50 +++++++++++++++++++ test/feature/query/inserts_new.spec.ts | 50 +++++++++++++++++++ test/feature/repositories/Crud_New.spec.ts | 29 ----------- 6 files changed, 151 insertions(+), 76 deletions(-) delete mode 100644 test/feature/basics/New.spec.ts create mode 100644 test/feature/models/static_inserts_new.spec.ts create mode 100644 test/feature/modules/api_inserts_new.spec.ts create mode 100644 test/feature/query/inserts_new.spec.ts delete mode 100644 test/feature/repositories/Crud_New.spec.ts diff --git a/src/model/Model.ts b/src/model/Model.ts index 0385ebfb..ccfb2f84 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -473,7 +473,7 @@ export default class Model { /** * Create new data with all fields filled by default values. */ - static new(): Promise { + static new(this: T): Promise> { return this.dispatch('new') } diff --git a/test/feature/basics/New.spec.ts b/test/feature/basics/New.spec.ts deleted file mode 100644 index 843852a4..00000000 --- a/test/feature/basics/New.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – New', () => { - class User extends Model { - static entity = 'users' - - // @Uid - id!: string - - // @Str('Default Doe') - name!: string - - static fields() { - return { - id: this.uid(), - name: this.attr('Default Doe') - } - } - } - - it('can create new data with all fields filled by default values', async () => { - const store = createStore([{ model: User }]) - - await User.new() - await User.new() - - expect(store.state.entities.users.data.$uid1.$id).toBe('$uid1') - expect(store.state.entities.users.data.$uid1.id).toBe('$uid1') - expect(store.state.entities.users.data.$uid1.name).toBe('Default Doe') - - expect(store.state.entities.users.data.$uid2.$id).toBe('$uid2') - expect(store.state.entities.users.data.$uid2.id).toBe('$uid2') - expect(store.state.entities.users.data.$uid2.name).toBe('Default Doe') - }) - - it('returns newly created instance', async () => { - createStore([{ model: User }]) - - const user = (await User.new()) as User - - expect(user.$id).toBe('$uid3') - expect(user.id).toBe('$uid3') - expect(user.name).toBe('Default Doe') - }) -}) diff --git a/test/feature/models/static_inserts_new.spec.ts b/test/feature/models/static_inserts_new.spec.ts new file mode 100644 index 00000000..1270857d --- /dev/null +++ b/test/feature/models/static_inserts_new.spec.ts @@ -0,0 +1,50 @@ +import { createStore, createState } from 'test/support/Helpers' +import Uid from '@/support/Uid' +import { Model } from '@/index' + +describe('feature/query/inserts_new', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.uid(), + name: this.attr('John Doe') + } + } + + id!: string + name!: string + } + + beforeEach(() => { + Uid.reset() + }) + + it('can create new data with all fields filled by default values', async () => { + const store = createStore([User]) + + await User.new() + await User.new() + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' }, + $uid2: { $id: '$uid2', id: '$uid2', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns a newly created model', async () => { + createStore([User]) + + const user = await User.new() + + expect(user).toBeInstanceOf(User) + expect(user.$id).toBe('$uid1') + expect(user.id).toBe('$uid1') + expect(user.name).toBe('John Doe') + }) +}) diff --git a/test/feature/modules/api_inserts_new.spec.ts b/test/feature/modules/api_inserts_new.spec.ts new file mode 100644 index 00000000..fe91042a --- /dev/null +++ b/test/feature/modules/api_inserts_new.spec.ts @@ -0,0 +1,50 @@ +import { createStore, createState } from 'test/support/Helpers' +import Uid from '@/support/Uid' +import { Model } from '@/index' + +describe('feature/modules/api_inserts_new', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.uid(), + name: this.attr('John Doe') + } + } + + id!: string + name!: string + } + + beforeEach(() => { + Uid.reset() + }) + + it('can create new data with all fields filled by default values', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/new') + await store.dispatch('entities/users/new') + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' }, + $uid2: { $id: '$uid2', id: '$uid2', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns a newly created model', async () => { + const store = createStore([User]) + + const user = await store.dispatch('entities/users/new') + + expect(user).toBeInstanceOf(User) + expect(user.$id).toBe('$uid1') + expect(user.id).toBe('$uid1') + expect(user.name).toBe('John Doe') + }) +}) diff --git a/test/feature/query/inserts_new.spec.ts b/test/feature/query/inserts_new.spec.ts new file mode 100644 index 00000000..6b88da7d --- /dev/null +++ b/test/feature/query/inserts_new.spec.ts @@ -0,0 +1,50 @@ +import { createStore, createState } from 'test/support/Helpers' +import Uid from '@/support/Uid' +import { Model } from '@/index' + +describe('feature/models/static_inserts_new', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.uid(), + name: this.attr('John Doe') + } + } + + id!: string + name!: string + } + + beforeEach(() => { + Uid.reset() + }) + + it('can create new data with all fields filled by default values', async () => { + const store = createStore([User]) + + await store.$repo(User).new() + await store.$repo(User).new() + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' }, + $uid2: { $id: '$uid2', id: '$uid2', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns a newly created model', async () => { + const store = createStore([User]) + + const user = await store.$repo(User).new() + + expect(user).toBeInstanceOf(User) + expect(user.$id).toBe('$uid1') + expect(user.id).toBe('$uid1') + expect(user.name).toBe('John Doe') + }) +}) diff --git a/test/feature/repositories/Crud_New.spec.ts b/test/feature/repositories/Crud_New.spec.ts deleted file mode 100644 index 2d86f5d3..00000000 --- a/test/feature/repositories/Crud_New.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model } from '@/index' - -describe('Feature - Repositories - CRUD New', () => { - it('can insert a new record to the store', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.uid(), - name: this.string('John Doe') - } - } - } - - const store = createStore([User]) - - await store.$repo(User).new() - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) From baf2b473cd07a690dea57afd8b09981b4e333b5e Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Mon, 6 Apr 2020 15:57:40 +0100 Subject: [PATCH 12/56] feat(Repository): migrate new persist options syntax (#617) Co-authored-by: Kia Ishii --- src/model/Model.ts | 30 ++-- src/model/support/Payloads.ts | 39 +++++ src/query/Query.ts | 31 ++-- src/repository/Repository.ts | 44 +++-- src/repository/support/Payloads.ts | 39 +++++ test/feature/basics/Create.spec.ts | 156 ----------------- test/feature/basics/Insert.spec.ts | 63 ------- .../models/static_inserts_create.spec.ts | 102 +++++++++++ .../models/static_inserts_insert.spec.ts | 103 +++++++++++ .../modules/api_inserts_create.spec.ts | 95 +++++++++++ .../modules/api_inserts_insert.spec.ts | 96 +++++++++++ test/feature/query/inserts_create.spec.ts | 160 ++++++++++++++++++ test/feature/query/inserts_insert.spec.ts | 160 ++++++++++++++++++ test/feature/repositories/Crud_Create.spec.ts | 31 ---- test/feature/repositories/Crud_Insert.spec.ts | 31 ---- test/support/Helpers.ts | 32 +++- 16 files changed, 888 insertions(+), 324 deletions(-) create mode 100644 src/model/support/Payloads.ts create mode 100644 src/repository/support/Payloads.ts delete mode 100644 test/feature/basics/Create.spec.ts delete mode 100644 test/feature/basics/Insert.spec.ts create mode 100644 test/feature/models/static_inserts_create.spec.ts create mode 100644 test/feature/models/static_inserts_insert.spec.ts create mode 100644 test/feature/modules/api_inserts_create.spec.ts create mode 100644 test/feature/modules/api_inserts_insert.spec.ts create mode 100644 test/feature/query/inserts_create.spec.ts create mode 100644 test/feature/query/inserts_insert.spec.ts delete mode 100644 test/feature/repositories/Crud_Create.spec.ts delete mode 100644 test/feature/repositories/Crud_Insert.spec.ts diff --git a/src/model/Model.ts b/src/model/Model.ts index ccfb2f84..71798d55 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -22,6 +22,8 @@ import ModelState from './contracts/State' import InheritanceTypes from './contracts/InheritanceTypes' import { toAttributes, toJson } from './Serialize' import PayloadBuilder from '../modules/support/PayloadBuilder' +import { PersistOptions as QueryPersistOptions } from '../query/options' +import { InsertObject, normalizeInsertPayload } from './support/Payloads' export default class Model { /** @@ -459,8 +461,8 @@ export default class Model { /** * Get query instance. */ - static query(this: T): Query> { - return this.getters('query')() + static query(this: M): Query> { + return new Query(this.store(), this.entity) } /** @@ -474,7 +476,7 @@ export default class Model { * Create new data with all fields filled by default values. */ static new(this: T): Promise> { - return this.dispatch('new') + return this.query().new() } /** @@ -482,23 +484,25 @@ export default class Model { * store. If you want to save data without replacing existing records, * use the `insert` method instead. */ - static create( - this: T, - payload: Payloads.Create, - options?: PersistOptions.Create + static create( + data: Record | Record[] | InsertObject, + options?: QueryPersistOptions ): Promise { - return this.dispatch('create', PayloadBuilder.normalize(payload, options)) + const { data: d, options: o } = normalizeInsertPayload(data, options) + + return this.query().create(d, o) } /** * Insert records. */ - static insert( - this: T, - payload: Payloads.Insert, - options?: PersistOptions.Insert + static insert( + data: Record | Record[] | InsertObject, + options?: QueryPersistOptions ): Promise { - return this.dispatch('insert', PayloadBuilder.normalize(payload, options)) + const { data: d, options: o } = normalizeInsertPayload(data, options) + + return this.query().insert(d, o) } /** diff --git a/src/model/support/Payloads.ts b/src/model/support/Payloads.ts new file mode 100644 index 00000000..69332c76 --- /dev/null +++ b/src/model/support/Payloads.ts @@ -0,0 +1,39 @@ +import { isArray } from '../../support/Utils' +import { Record } from '../../data' +import { PersistOptions } from '../../query/options' + +export interface InsertObject extends PersistOptions { + data: Record | Record[] +} + +export interface InsertPayload { + data: Record | Record[] + options?: PersistOptions +} + +/** + * Normalize the payload arguments for persist methods. It's required for + * backward compatibility for the new persist method argument structure. + * It may be deprecated and be removed in the future. + */ +export function normalizeInsertPayload( + data: Record | Record[] | InsertObject, + options?: PersistOptions +): InsertPayload { + // If the data is an array, then the data should be an array of data. + // Therefore the whole payload should be passed directly as `data`. + if (isArray(data)) { + return { data, options } + } + + // If the data doesn't have a `data` property, it can be assumed that the + // user has passed an object as the payload. Therefore the entire payload + // should be declared as `data`. + if (data.data === undefined) { + return { data, options } + } + + // If the data contains `data` property, we should extract it and pass that + // property as `data`. + return { data: data.data, options: data } +} diff --git a/src/query/Query.ts b/src/query/Query.ts index e873f626..7add3be0 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -795,7 +795,7 @@ export default class Query { /** * Create new data with all fields filled by default values. */ - new(): T { + async new(): Promise { const model = new this.model().$generateId() as T this.commitInsert(model.$getAttributes()) @@ -810,8 +810,8 @@ export default class Query { */ create( data: Data.Record | Data.Record[], - options: Options.PersistOptions - ): Data.Collections { + options?: Options.PersistOptions + ): Promise { return this.persist('create', data, options) } @@ -831,8 +831,8 @@ export default class Query { */ insert( data: Data.Record | Data.Record[], - options: Options.PersistOptions - ): Data.Collections { + options?: Options.PersistOptions + ): Promise { return this.persist('insert', data, options) } @@ -858,7 +858,10 @@ export default class Query { data: Data.Record | Data.Record[] | UpdateClosure, condition: UpdateCondition, options: Options.PersistOptions - ): Data.Item | Data.Collection | Data.Collections { + ): + | Promise> + | Promise> + | Promise { // If the data is array, simply normalize the data and update them. if (Utils.isArray(data)) { return this.persist('update', data, options) @@ -925,10 +928,10 @@ export default class Query { /** * Update the state by id. */ - updateById( + async updateById( data: Data.Record | UpdateClosure, id: string | number - ): Data.Item { + ): Promise> { id = typeof id === 'number' ? id.toString() : this.normalizeIndexId(id) const record = this.state.data[id] @@ -951,10 +954,10 @@ export default class Query { /** * Update the state by condition. */ - updateByCondition( + async updateByCondition( data: Data.Record | UpdateClosure, condition: Contracts.Predicate - ): Data.Collection { + ): Promise> { const instances = Object.keys(this.state.data).reduce>( (instances, id) => { const instance = this.hydrate(this.state.data[id]) @@ -1066,7 +1069,7 @@ export default class Query { insertOrUpdate( data: Data.Record | Data.Record[], options: Options.PersistOptions - ): Data.Collections { + ): Promise { return this.persist('insertOrUpdate', data, options) } @@ -1098,11 +1101,11 @@ export default class Query { /** * Persist data into the state while preserving it's original structure. */ - persist( + async persist( method: Options.PersistMethods, data: Data.Record | Data.Record[], - options: Options.PersistOptions - ): Data.Collections { + options: Options.PersistOptions = {} + ): Promise { const clonedData = Utils.cloneDeep(data) const normalizedData = this.normalize(clonedData) diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 6c34c6bc..9f81ab90 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -4,8 +4,11 @@ import Database from '../database/Database' import { Record, Item, Collection, Collections } from '../data' import Model from '../model/Model' import * as Payloads from '../modules/payloads/Actions' +import PayloadBuilder from '../modules/support/PayloadBuilder' import Query from '../query/Query' import Predicate from '../query/contracts/Predicate' +import { PersistOptions } from '../query/options' +import { InsertObject, normalizeInsertPayload } from './support/Payloads' interface ConstructorOf extends BaseConstructorOf { entity: string @@ -56,7 +59,7 @@ export default class Repository { } /** - * Call Vuex Getetrs. + * Call Vuex Getters. */ getters(method: string): any { return this.store.getters[this.namespace(method)] @@ -101,7 +104,7 @@ export default class Repository { * Get query instance. */ query(): Query { - return this.getters('query')() + return new Query(this.store, this.model.entity) } /** @@ -115,7 +118,7 @@ export default class Repository { * Create new data with all fields filled by default values. */ new(): Promise { - return this.dispatch('new') + return this.query().new() } /** @@ -123,29 +126,48 @@ export default class Repository { * store. If you want to save data without replacing existing records, * use the `insert` method instead. */ - create(payload: Payloads.Create): Promise { - return this.dispatch('create', payload) + create( + data: Record | Record[] | InsertObject, + options?: PersistOptions + ): Promise { + const { data: d, options: o } = normalizeInsertPayload(data, options) + + return this.query().create(d, o) } /** * Insert records. */ - insert(payload: Payloads.Insert): Promise { - return this.dispatch('insert', payload) + insert( + data: Record | Record[] | InsertObject, + options?: PersistOptions + ): Promise { + const { data: d, options: o } = normalizeInsertPayload(data, options) + + return this.query().insert(d, o) } /** * Update records. */ - update(payload: Payloads.Update): Promise { - return this.dispatch('update', payload) + update( + payload: Payloads.Update, + options?: PersistOptions + ): Promise { + return this.dispatch('update', PayloadBuilder.normalize(payload, options)) } /** * Insert or update records. */ - insertOrUpdate(payload: Payloads.InsertOrUpdate): Promise { - return this.dispatch('insertOrUpdate', payload) + insertOrUpdate( + payload: Payloads.InsertOrUpdate, + options?: PersistOptions + ): Promise { + return this.dispatch( + 'insertOrUpdate', + PayloadBuilder.normalize(payload, options) + ) } /** diff --git a/src/repository/support/Payloads.ts b/src/repository/support/Payloads.ts new file mode 100644 index 00000000..69332c76 --- /dev/null +++ b/src/repository/support/Payloads.ts @@ -0,0 +1,39 @@ +import { isArray } from '../../support/Utils' +import { Record } from '../../data' +import { PersistOptions } from '../../query/options' + +export interface InsertObject extends PersistOptions { + data: Record | Record[] +} + +export interface InsertPayload { + data: Record | Record[] + options?: PersistOptions +} + +/** + * Normalize the payload arguments for persist methods. It's required for + * backward compatibility for the new persist method argument structure. + * It may be deprecated and be removed in the future. + */ +export function normalizeInsertPayload( + data: Record | Record[] | InsertObject, + options?: PersistOptions +): InsertPayload { + // If the data is an array, then the data should be an array of data. + // Therefore the whole payload should be passed directly as `data`. + if (isArray(data)) { + return { data, options } + } + + // If the data doesn't have a `data` property, it can be assumed that the + // user has passed an object as the payload. Therefore the entire payload + // should be declared as `data`. + if (data.data === undefined) { + return { data, options } + } + + // If the data contains `data` property, we should extract it and pass that + // property as `data`. + return { data: data.data, options: data } +} diff --git a/test/feature/basics/Create.spec.ts b/test/feature/basics/Create.spec.ts deleted file mode 100644 index 17024030..00000000 --- a/test/feature/basics/Create.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Create', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('JD') - } - } - } - - it('can create a record', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const expected = { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - - expect(store.state.entities.users.data[1]).not.toBeInstanceOf(User) - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('can create a list of records', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const expected = { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('replaces any existing records', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - await store.dispatch('entities/users/create', { - data: { id: 2, name: 'Jane Doe' } - }) - - const expected = { - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('cleans all existing records when passing empty object', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - await store.dispatch('entities/users/create', { - data: {} - }) - - const expected = {} - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('cleans all existing records when passing empty array', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - await store.dispatch('entities/users/create', { - data: [] - }) - - const expected = {} - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('fills missing fields with the default value', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1 } - }) - - const expected = { - 1: { $id: '1', id: 1, name: 'JD' } - } - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('returns a newly created record', async () => { - const store = createStore([{ model: User }]) - - const collection = await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const expected = { - users: [new User({ $id: '1', id: 1, name: 'John Doe' })] - } - - expect(collection).toEqual(expected) - }) - - it('returns list of newly created record', async () => { - const store = createStore([{ model: User }]) - - const collection = await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const expected = { - users: [ - new User({ $id: '1', id: 1, name: 'John Doe' }), - new User({ $id: '2', id: 2, name: 'Jane Doe' }) - ] - } - - expect(collection).toEqual(expected) - }) - - it('returns null when creating an empty record', async () => { - const store = createStore([{ model: User }]) - - const collection = await store.dispatch('entities/users/create', { - data: {} - }) - - expect(collection).toEqual({}) - }) -}) diff --git a/test/feature/basics/Insert.spec.ts b/test/feature/basics/Insert.spec.ts deleted file mode 100644 index 8d5db833..00000000 --- a/test/feature/basics/Insert.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Insert', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('John Doe') - } - } - } - - const getStore = () => createStore([{ model: User }]) - - it('can insert a record', async () => { - const store = getStore() - - await store.dispatch('entities/users/insert', { - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('does nothing if an empty object is passed', async () => { - const store = getStore() - - await store.dispatch('entities/users/insert', { - data: {} - }) - - const expected = createState({ - users: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert record with primary key value of `null`', async () => { - const store = getStore() - - await store.dispatch('entities/users/insert', { - data: { id: null, name: 'John Doe' } - }) - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/models/static_inserts_create.spec.ts b/test/feature/models/static_inserts_create.spec.ts new file mode 100644 index 00000000..1e947c6b --- /dev/null +++ b/test/feature/models/static_inserts_create.spec.ts @@ -0,0 +1,102 @@ +import { + createStore, + createState, + serializeCollection +} from 'test/support/Helpers' +import { Model } from '@/index' + +describe('feature/models/static_inserts_create', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + it('can create a record', async () => { + const store = createStore([User]) + + await User.create({ id: 1, name: 'John Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can create multiple records', async () => { + const store = createStore([User]) + + await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('replaces any existing records', async () => { + const store = createStore([User]) + + await User.create({ id: 1, name: 'John Doe' }) + await User.create({ id: 2, name: 'Jane Doe' }) + + const expected = createState({ + users: { + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns newly created data', async () => { + createStore([User]) + + const result = await User.create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = [ + { $id: '1', id: 1, name: 'John Doe' }, + { $id: '2', id: 2, name: 'Jane Doe' } + ] + + expect(result.users.length).toBe(2) + expect(result.users[0]).toBeInstanceOf(User) + expect(result.users[1]).toBeInstanceOf(User) + expect(serializeCollection(result.users)).toEqual(expected) + }) + + // Test for deprecated payload API syntax. + it('accepts the "payload" object as an argument', async () => { + const store = createStore([User]) + + await User.create({ + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/models/static_inserts_insert.spec.ts b/test/feature/models/static_inserts_insert.spec.ts new file mode 100644 index 00000000..90e7ee0a --- /dev/null +++ b/test/feature/models/static_inserts_insert.spec.ts @@ -0,0 +1,103 @@ +import { + createStore, + createState, + serializeCollection +} from 'test/support/Helpers' +import { Model } from '@/index' + +describe('feature/models/static_inserts_insert', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + it('can insert a record', async () => { + const store = createStore([User]) + + await User.insert({ id: 1, name: 'John Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can insert multiple records', async () => { + const store = createStore([User]) + + await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('keeps existing records', async () => { + const store = createStore([User]) + + await User.insert({ id: 1, name: 'John Doe' }) + await User.insert({ id: 2, name: 'Jane Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns newly created data', async () => { + createStore([User]) + + const result = await User.insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = [ + { $id: '1', id: 1, name: 'John Doe' }, + { $id: '2', id: 2, name: 'Jane Doe' } + ] + + expect(result.users.length).toBe(2) + expect(result.users[0]).toBeInstanceOf(User) + expect(result.users[1]).toBeInstanceOf(User) + expect(serializeCollection(result.users)).toEqual(expected) + }) + + // Test for deprecated payload API syntax. + it('accepts the "payload" object as an argument', async () => { + const store = createStore([User]) + + await User.insert({ + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/modules/api_inserts_create.spec.ts b/test/feature/modules/api_inserts_create.spec.ts new file mode 100644 index 00000000..fd9ea8e8 --- /dev/null +++ b/test/feature/modules/api_inserts_create.spec.ts @@ -0,0 +1,95 @@ +import { + createStore, + createState, + serializeCollection +} from 'test/support/Helpers' +import { Model } from '@/index' + +describe('feature/modules/api_inserts_create', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + it('can create a record', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/create', { + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can create multiple records', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/create', { + data: [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ] + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('replaces any existing records', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/create', { + data: { id: 1, name: 'John Doe' } + }) + await store.dispatch('entities/users/create', { + data: { id: 2, name: 'Jane Doe' } + }) + + const expected = createState({ + users: { + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns newly created data', async () => { + const store = createStore([User]) + + const result = await store.dispatch('entities/users/create', { + data: [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ] + }) + + const expected = [ + { $id: '1', id: 1, name: 'John Doe' }, + { $id: '2', id: 2, name: 'Jane Doe' } + ] + + expect(result.users.length).toBe(2) + expect(result.users[0]).toBeInstanceOf(User) + expect(result.users[1]).toBeInstanceOf(User) + expect(serializeCollection(result.users)).toEqual(expected) + }) +}) diff --git a/test/feature/modules/api_inserts_insert.spec.ts b/test/feature/modules/api_inserts_insert.spec.ts new file mode 100644 index 00000000..06645906 --- /dev/null +++ b/test/feature/modules/api_inserts_insert.spec.ts @@ -0,0 +1,96 @@ +import { + createStore, + createState, + serializeCollection +} from 'test/support/Helpers' +import { Model } from '@/index' + +describe('feature/modules/inserts_insert', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.string('') + } + } + } + + it('can insert a record', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/insert', { + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can insert multiple records', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/insert', { + data: [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ] + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('keeps existing records', async () => { + const store = createStore([User]) + + await store.dispatch('entities/users/insert', { + data: { id: 1, name: 'John Doe' } + }) + await store.dispatch('entities/users/insert', { + data: { id: 2, name: 'Jane Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns newly created data', async () => { + const store = createStore([User]) + + const result = await store.dispatch('entities/users/insert', { + data: [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ] + }) + + const expected = [ + { $id: '1', id: 1, name: 'John Doe' }, + { $id: '2', id: 2, name: 'Jane Doe' } + ] + + expect(result.users.length).toBe(2) + expect(result.users[0]).toBeInstanceOf(User) + expect(result.users[1]).toBeInstanceOf(User) + expect(serializeCollection(result.users)).toEqual(expected) + }) +}) diff --git a/test/feature/query/inserts_create.spec.ts b/test/feature/query/inserts_create.spec.ts new file mode 100644 index 00000000..15cbc475 --- /dev/null +++ b/test/feature/query/inserts_create.spec.ts @@ -0,0 +1,160 @@ +import { + createStore, + createState, + serializeCollection +} from 'test/support/Helpers' +import Uid from '@/support/Uid' +import { Model } from '@/index' + +describe('feature/query/inserts_create', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.string('Default Doe') + } + } + } + + beforeEach(() => { + Uid.reset() + }) + + it('can create a record', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ id: 1, name: 'John Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can create multiple records', async () => { + const store = createStore([User]) + + await store.$repo(User).create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('replaces any existing records', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ id: 1, name: 'John Doe' }) + await store.$repo(User).create({ id: 2, name: 'Jane Doe' }) + + const expected = createState({ + users: { + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('cleans all existing records if the empty data is passed', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ id: 1, name: 'John Doe' }) + await store.$repo(User).create({}) + + const expected = createState({ + users: {} + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('fills missing fields with the default value', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ id: 1 }) + + const expected = { + 1: { $id: '1', id: 1, name: 'Default Doe' } + } + + expect(store.state.entities.users.data).toEqual(expected) + }) + + it('can insert record with missing primary key value', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ name: 'John Doe' }) + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can insert record with primary key value of `null`', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ id: null, name: 'John Doe' }) + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns newly created data', async () => { + const store = createStore([User]) + + const result = await store.$repo(User).create([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = [ + { $id: '1', id: 1, name: 'John Doe' }, + { $id: '2', id: 2, name: 'Jane Doe' } + ] + + expect(result.users.length).toBe(2) + expect(result.users[0]).toBeInstanceOf(User) + expect(result.users[1]).toBeInstanceOf(User) + expect(serializeCollection(result.users)).toEqual(expected) + }) + + // Test for deprecated payload API syntax. + it('accepts the "payload" object as an argument', async () => { + const store = createStore([User]) + + await store.$repo(User).create({ + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/query/inserts_insert.spec.ts b/test/feature/query/inserts_insert.spec.ts new file mode 100644 index 00000000..afd6f247 --- /dev/null +++ b/test/feature/query/inserts_insert.spec.ts @@ -0,0 +1,160 @@ +import { + createStore, + createState, + serializeCollection +} from 'test/support/Helpers' +import Uid from '@/support/Uid' +import { Model } from '@/index' + +describe('feature/query/inserts_insert', () => { + class User extends Model { + static entity = 'users' + + static fields() { + return { + id: this.attr(null), + name: this.string('Default Doe') + } + } + } + + beforeEach(() => { + Uid.reset() + }) + + it('does nothing if the given data is empty', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({}) + + const expected = createState({ + users: {} + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can insert a record', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ id: 1, name: 'John Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can insert multiple records', async () => { + const store = createStore([User]) + + await store.$repo(User).insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('keeps existing records', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ id: 1, name: 'John Doe' }) + await store.$repo(User).insert({ id: 2, name: 'Jane Doe' }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' }, + 2: { $id: '2', id: 2, name: 'Jane Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('fills missing fields with the default value', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ id: 1 }) + + const expected = { + 1: { $id: '1', id: 1, name: 'Default Doe' } + } + + expect(store.state.entities.users.data).toEqual(expected) + }) + + it('can insert record with missing primary key value', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ name: 'John Doe' }) + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('can insert record with primary key value of `null`', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ id: null, name: 'John Doe' }) + + const expected = createState({ + users: { + $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) + + it('returns newly created data', async () => { + const store = createStore([User]) + + const result = await store.$repo(User).insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) + + const expected = [ + { $id: '1', id: 1, name: 'John Doe' }, + { $id: '2', id: 2, name: 'Jane Doe' } + ] + + expect(result.users.length).toBe(2) + expect(result.users[0]).toBeInstanceOf(User) + expect(result.users[1]).toBeInstanceOf(User) + expect(serializeCollection(result.users)).toEqual(expected) + }) + + // Test for deprecated payload API syntax. + it('accepts the "payload" object as an argument', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ + data: { id: 1, name: 'John Doe' } + }) + + const expected = createState({ + users: { + 1: { $id: '1', id: 1, name: 'John Doe' } + } + }) + + expect(store.state.entities).toEqual(expected) + }) +}) diff --git a/test/feature/repositories/Crud_Create.spec.ts b/test/feature/repositories/Crud_Create.spec.ts deleted file mode 100644 index 47f4b579..00000000 --- a/test/feature/repositories/Crud_Create.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model } from '@/index' - -describe('Feature - Repositories - CRUD Create', () => { - it('can create given record to the store', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - const store = createStore([User]) - - await store.$repo(User).create({ - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/repositories/Crud_Insert.spec.ts b/test/feature/repositories/Crud_Insert.spec.ts deleted file mode 100644 index 6f549819..00000000 --- a/test/feature/repositories/Crud_Insert.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model } from '@/index' - -describe('Feature - Repositories - CRUD Insert', () => { - it('can insert given record to the store', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - const store = createStore([User]) - - await store.$repo(User).insert({ - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/support/Helpers.ts b/test/support/Helpers.ts index 39550e4a..c0995207 100644 --- a/test/support/Helpers.ts +++ b/test/support/Helpers.ts @@ -2,6 +2,7 @@ import Vue from 'vue' import Vuex, { Store, Module } from 'vuex' import VuexORM from '@/index' import { mapValues } from '@/support/Utils' +import { Record, Collection } from '@/data' import Database from '@/database/Database' import Model from '@/model/Model' import State from '@/modules/contracts/State' @@ -51,13 +52,24 @@ export function createStoreFromDatabase(database: Database): Store { }) } +/** + * Fill store data with the given state. + */ +export function fillState(store: Store, state: Record): void { + for (const entity in state) { + store.commit('entities/$mutate', { + entity, + callback(s: State) { + s.data = state[entity] + } + }) + } +} + /** * Create a new entity state. */ -export function createState( - state: Record, - namespace: string = 'entities' -) { +export function createState(state: Record, namespace: string = 'entities') { return { $name: namespace, ...mapValues(state, (data, name) => ({ @@ -68,8 +80,18 @@ export function createState( } } +/** + * Serialize given collection by invoking `$getAttributes` on each model + * in the given collection. + */ +export function serializeCollection(collection: Collection): Record[] { + return collection.map((m) => m.$getAttributes()) +} + export default { createStore, createStoreFromDatabase, - createState + fillState, + createState, + serializeCollection } From 135664a013e24e42e08ffec946fcadad75073699 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Mon, 6 Apr 2020 04:22:19 +0100 Subject: [PATCH 13/56] feat(Repository): migrate new persist options syntax --- src/repository/Repository.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 9f81ab90..8080142b 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -4,11 +4,10 @@ import Database from '../database/Database' import { Record, Item, Collection, Collections } from '../data' import Model from '../model/Model' import * as Payloads from '../modules/payloads/Actions' +import * as PersistOptions from '../modules/options/Actions' import PayloadBuilder from '../modules/support/PayloadBuilder' import Query from '../query/Query' import Predicate from '../query/contracts/Predicate' -import { PersistOptions } from '../query/options' -import { InsertObject, normalizeInsertPayload } from './support/Payloads' interface ConstructorOf extends BaseConstructorOf { entity: string @@ -104,7 +103,7 @@ export default class Repository { * Get query instance. */ query(): Query { - return new Query(this.store, this.model.entity) + return this.getters('query')() } /** @@ -118,7 +117,7 @@ export default class Repository { * Create new data with all fields filled by default values. */ new(): Promise { - return this.query().new() + return this.dispatch('new') } /** @@ -127,24 +126,20 @@ export default class Repository { * use the `insert` method instead. */ create( - data: Record | Record[] | InsertObject, - options?: PersistOptions + payload: Payloads.Create, + options?: PersistOptions.Create ): Promise { - const { data: d, options: o } = normalizeInsertPayload(data, options) - - return this.query().create(d, o) + return this.dispatch('create', PayloadBuilder.normalize(payload, options)) } /** * Insert records. */ insert( - data: Record | Record[] | InsertObject, - options?: PersistOptions + payload: Payloads.Insert, + options?: PersistOptions.Insert ): Promise { - const { data: d, options: o } = normalizeInsertPayload(data, options) - - return this.query().insert(d, o) + return this.dispatch('insert', PayloadBuilder.normalize(payload, options)) } /** @@ -152,7 +147,7 @@ export default class Repository { */ update( payload: Payloads.Update, - options?: PersistOptions + options?: PersistOptions.Insert ): Promise { return this.dispatch('update', PayloadBuilder.normalize(payload, options)) } @@ -162,7 +157,7 @@ export default class Repository { */ insertOrUpdate( payload: Payloads.InsertOrUpdate, - options?: PersistOptions + options?: PersistOptions.Insert ): Promise { return this.dispatch( 'insertOrUpdate', From cfb5c18d2face52b5502d8067131aaf404a81500 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Mon, 6 Apr 2020 18:56:25 +0900 Subject: [PATCH 14/56] refactor: align repository create/insert api with the latest spec --- src/repository/Repository.ts | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 8080142b..9f81ab90 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -4,10 +4,11 @@ import Database from '../database/Database' import { Record, Item, Collection, Collections } from '../data' import Model from '../model/Model' import * as Payloads from '../modules/payloads/Actions' -import * as PersistOptions from '../modules/options/Actions' import PayloadBuilder from '../modules/support/PayloadBuilder' import Query from '../query/Query' import Predicate from '../query/contracts/Predicate' +import { PersistOptions } from '../query/options' +import { InsertObject, normalizeInsertPayload } from './support/Payloads' interface ConstructorOf extends BaseConstructorOf { entity: string @@ -103,7 +104,7 @@ export default class Repository { * Get query instance. */ query(): Query { - return this.getters('query')() + return new Query(this.store, this.model.entity) } /** @@ -117,7 +118,7 @@ export default class Repository { * Create new data with all fields filled by default values. */ new(): Promise { - return this.dispatch('new') + return this.query().new() } /** @@ -126,20 +127,24 @@ export default class Repository { * use the `insert` method instead. */ create( - payload: Payloads.Create, - options?: PersistOptions.Create + data: Record | Record[] | InsertObject, + options?: PersistOptions ): Promise { - return this.dispatch('create', PayloadBuilder.normalize(payload, options)) + const { data: d, options: o } = normalizeInsertPayload(data, options) + + return this.query().create(d, o) } /** * Insert records. */ insert( - payload: Payloads.Insert, - options?: PersistOptions.Insert + data: Record | Record[] | InsertObject, + options?: PersistOptions ): Promise { - return this.dispatch('insert', PayloadBuilder.normalize(payload, options)) + const { data: d, options: o } = normalizeInsertPayload(data, options) + + return this.query().insert(d, o) } /** @@ -147,7 +152,7 @@ export default class Repository { */ update( payload: Payloads.Update, - options?: PersistOptions.Insert + options?: PersistOptions ): Promise { return this.dispatch('update', PayloadBuilder.normalize(payload, options)) } @@ -157,7 +162,7 @@ export default class Repository { */ insertOrUpdate( payload: Payloads.InsertOrUpdate, - options?: PersistOptions.Insert + options?: PersistOptions ): Promise { return this.dispatch( 'insertOrUpdate', From cc0da6256679be551f0996bee8b1a535fd2eceaf Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Mon, 6 Apr 2020 23:55:40 +0900 Subject: [PATCH 15/56] wip: working on --- src/connections/Connection.ts | 37 +++++++++++++++++++++++--- src/data/RecordWithId.ts | 7 +++++ src/data/index.ts | 2 ++ src/model/Model.ts | 3 ++- src/model/Serialize.ts | 6 ++--- src/modules/RootMutations.ts | 25 ++++++++++++------ src/modules/payloads/RootMutations.ts | 16 +++++------ src/query/Query.ts | 38 +++++++++++---------------- 8 files changed, 86 insertions(+), 48 deletions(-) create mode 100644 src/data/RecordWithId.ts diff --git a/src/connections/Connection.ts b/src/connections/Connection.ts index 4db34830..75ddcb8c 100644 --- a/src/connections/Connection.ts +++ b/src/connections/Connection.ts @@ -1,6 +1,5 @@ import { Store } from 'vuex' -import Record from '../data/Record' -import Records from '../data/Records' +import { Records, RecordWithId } from '../data' import RootState from '../modules/contracts/RootState' import State from '../modules/contracts/State' @@ -41,11 +40,31 @@ export default class Connection { this.state = this.rootState[entity] } + /** + * Commit Vuex Mutation. + */ + private commit(name: string, payload: any): void { + this.store.commit(`${this.connection}/${name}`, payload) + } + + /** + * Commit the given record by `new` method. + */ + new(record: RecordWithId): void { + this.commit('new', { + entity: this.entity, + data: { [record.$id]: record } + }) + } + /** * Insert the given record. */ - insert(record: Record): void { - this.state.data = { ...this.state.data, [record.$id]: record } + insert(records: RecordWithId[]): void { + this.commit('new', { + entity: this.entity, + data: this.mapRecordToRecords(records) + }) } /** @@ -69,4 +88,14 @@ export default class Connection { this.state.data = data } + + /** + * Map the given records to `Records` type object. + */ + private mapRecordToRecords(records: RecordWithId[]): Records { + return records.reduce((carry, record) => { + carry[record.$id] = record + return carry + }, {}) + } } diff --git a/src/data/RecordWithId.ts b/src/data/RecordWithId.ts new file mode 100644 index 00000000..60c7b94c --- /dev/null +++ b/src/data/RecordWithId.ts @@ -0,0 +1,7 @@ +import Record from './Record' + +export interface RecordWithId extends Record { + $id: string +} + +export default RecordWithId diff --git a/src/data/index.ts b/src/data/index.ts index 0cb78956..19fb4667 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,4 +1,5 @@ import Record from './Record' +import RecordWithId from './RecordWithId' import Records from './Records' import NormalizedData from './NormalizedData' import Instance from './Instance' @@ -11,6 +12,7 @@ import Collections from './Collections' export { Record, + RecordWithId, Records, NormalizedData, InstanceOf, diff --git a/src/model/Model.ts b/src/model/Model.ts index 71798d55..43f83b2f 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -4,6 +4,7 @@ import Uid from '../support/Uid' import Container from '../container/Container' import Database from '../database/Database' import Record from '../data/Record' +import RecordWithId from '../data/RecordWithId' import InstanceOf from '../data/InstanceOf' import Item from '../data/Item' import Collection from '../data/Collection' @@ -1062,7 +1063,7 @@ export default class Model { * value as well. This method is mainly used when saving a model to * the store. */ - $getAttributes(): Record { + $getAttributes(): RecordWithId { return toAttributes(this) } diff --git a/src/model/Serialize.ts b/src/model/Serialize.ts index 8e1bf6ee..a07de0d3 100644 --- a/src/model/Serialize.ts +++ b/src/model/Serialize.ts @@ -1,5 +1,5 @@ import { isArray } from '../support/Utils' -import Record from '../data/Record' +import { Record, RecordWithId } from '../data' import Item from '../data/Item' import Collection from '../data/Collection' import Relation from '../attributes/relations/Relation' @@ -17,12 +17,12 @@ const defaultOption: Option = { * Serialize the given model to attributes. This method will ignore * relationships, and it includes the index id. */ -export function toAttributes(model: Model): Record { +export function toAttributes(model: Model): RecordWithId { const record = toJson(model, { relations: false }) record.$id = model.$id - return record + return record as RecordWithId } /** diff --git a/src/modules/RootMutations.ts b/src/modules/RootMutations.ts index c5223279..dc02ca3b 100644 --- a/src/modules/RootMutations.ts +++ b/src/modules/RootMutations.ts @@ -1,6 +1,6 @@ import { Store } from 'vuex' import Connection from '../connections/Connection' -import RootState from './contracts/RootState' +import State from './contracts/RootState' import MutationsContract from './contracts/RootMutations' import * as Payloads from './payloads/RootMutations' @@ -10,24 +10,32 @@ import * as Payloads from './payloads/RootMutations' */ function $mutate( this: Store, - state: RootState, + state: State, payload: Payloads.$Mutate ): void { payload.callback(state[payload.entity]) } /** - * Insert the given record. + * Commit the given records by `new` method. */ -function insert(this: Store, state: RootState, payload: any): void { - const { entity, record } = payload - new Connection(this, state.$name, entity).insert(record) +function newRecord(state: State, payload: Payloads.New): void { + const entity = state[payload.entity] + entity.data = { ...entity.data, ...payload.data } +} + +/** + * Commit the given records by `insert` method. + */ +function insert(state: State, payload: Payloads.Insert): void { + const entity = state[payload.entity] + entity.data = { ...entity.data, ...payload.data } } /** * Insert the given records. */ -function insertRecords(this: Store, state: RootState, payload: any): void { +function insertRecords(this: Store, state: State, payload: any): void { const { entity, records } = payload new Connection(this, state.$name, entity).insertRecords(records) } @@ -37,13 +45,14 @@ function insertRecords(this: Store, state: RootState, payload: any): void { * `delete`, but named `destroy` here because `delete` can't be declared at * this scope level. */ -function destroy(this: Store, state: RootState, payload: any): void { +function destroy(this: Store, state: State, payload: any): void { const { entity, id } = payload new Connection(this, state.$name, entity).delete(id) } const RootMutations: MutationsContract = { $mutate, + new: newRecord, insert, insertRecords, delete: destroy diff --git a/src/modules/payloads/RootMutations.ts b/src/modules/payloads/RootMutations.ts index 7e52bdfb..e9ec3684 100644 --- a/src/modules/payloads/RootMutations.ts +++ b/src/modules/payloads/RootMutations.ts @@ -1,7 +1,7 @@ -import Record from '../../data/Record' +import { Record, Records } from '../../data' import Predicate from '../../query/contracts/Predicate' -import State from '../contracts/State' import PersistOptions from '../../query/options/PersistOptions' +import State from '../contracts/State' export type Condition = (record: Record) => boolean @@ -10,18 +10,14 @@ export interface $Mutate { callback: (state: State) => void } -export interface New extends PersistOptions { +export interface New { entity: string + data: Records } -export interface Create extends PersistOptions { +export interface Insert { entity: string - data: Record | Record[] -} - -export interface Insert extends PersistOptions { - entity: string - data: Record | Record[] + data: Records } export interface Update extends PersistOptions { diff --git a/src/query/Query.ts b/src/query/Query.ts index 7add3be0..e17fd597 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -1,6 +1,7 @@ import { Store } from 'vuex' import Utils from '../support/Utils' import Database, { Models } from '../database/Database' +import Connection from '../connections/Connection' import * as Data from '../data' import Model from '../model/Model' import State from '../modules/contracts/State' @@ -217,6 +218,17 @@ export default class Query { return new Query(this.store, entity) } + /** + * Get the new connection instance. + */ + connection(): Connection { + return new Connection( + this.store, + this.database.namespace, + this.entity + ) + } + /** * Get model of given name from the container. */ @@ -798,7 +810,7 @@ export default class Query { async new(): Promise { const model = new this.model().$generateId() as T - this.commitInsert(model.$getAttributes()) + this.connection().new(model.$getAttributes()) return model } @@ -844,7 +856,9 @@ export default class Query { collection = this.executeMutationHooks('beforeCreate', collection) - this.commitInsertRecords(this.convertCollectionToRecords(collection)) + this.connection().insert( + collection.map((m) => m.$getAttributes()) + ) this.executeMutationHooks('afterCreate', collection) @@ -1265,13 +1279,6 @@ export default class Query { this.commit('insert', { record }) } - /** - * Commit insert records mutation. - */ - private commitInsertRecords(records: Data.Record): void { - this.commit('insertRecords', { records }) - } - /** * Commit delete mutation. */ @@ -1369,19 +1376,6 @@ export default class Query { return collection } - /** - * Convert given collection to records by using index id as a key. - */ - private convertCollectionToRecords( - collection: Data.Collection - ): Data.Records { - return collection.reduce((carry, model) => { - carry[model['$id'] as string] = model.$getAttributes() - - return carry - }, {}) - } - /** * Clears the current state from any data related to current model. * From ee8e9f874aae52706f1cb55fd90267692cb17ae2 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 00:08:23 +0900 Subject: [PATCH 16/56] refresh --- src/attributes/Attribute.ts | 26 - src/attributes/contracts/DictionaryMany.ts | 7 - src/attributes/contracts/DictionaryOne.ts | 7 - src/attributes/contracts/Mutator.ts | 3 - src/attributes/contracts/Mutators.ts | 7 - src/attributes/index.ts | 43 - src/attributes/relations/BelongsTo.ts | 128 -- src/attributes/relations/BelongsToMany.ts | 257 --- src/attributes/relations/HasMany.ts | 132 -- src/attributes/relations/HasManyBy.ts | 119 -- src/attributes/relations/HasManyThrough.ts | 156 -- src/attributes/relations/HasOne.ts | 132 -- src/attributes/relations/HasOneThrough.ts | 151 -- src/attributes/relations/MorphMany.ts | 117 -- src/attributes/relations/MorphOne.ts | 113 -- src/attributes/relations/MorphTo.ts | 101 -- src/attributes/relations/MorphToMany.ts | 258 --- src/attributes/relations/MorphedByMany.ts | 248 --- src/attributes/relations/Relation.ts | 192 --- src/attributes/types/Attr.ts | 31 - src/attributes/types/Boolean.ts | 58 - src/attributes/types/Number.ts | 52 - src/attributes/types/String.ts | 44 - src/attributes/types/Type.ts | 43 - src/attributes/types/Uid.ts | 28 - src/connections/Connection.ts | 101 -- src/container/Container.ts | 15 - src/data/Collection.ts | 6 - src/data/Collections.ts | 7 - src/data/Entities.ts | 7 - src/data/Instance.ts | 5 - src/data/InstanceOf.ts | 3 - src/data/Instances.ts | 8 - src/data/Item.ts | 6 - src/data/NormalizedData.ts | 7 - src/data/Record.ts | 5 - src/data/RecordWithId.ts | 7 - src/data/Records.ts | 7 - src/data/index.ts | 25 - src/database/Database.ts | 403 ----- src/database/HackedDatabase.ts | 28 - src/index.cjs.ts | 70 +- src/index.ts | 129 +- src/model/Model.ts | 1076 ------------ src/model/Serialize.ts | 116 -- src/model/contracts/FieldCache.ts | 7 - src/model/contracts/Fields.ts | 7 - src/model/contracts/InheritanceTypes.ts | 7 - src/model/contracts/State.ts | 5 - src/model/support/Payloads.ts | 39 - src/modules/Actions.ts | 151 -- src/modules/Getters.ts | 63 - src/modules/RootActions.ts | 142 -- src/modules/RootGetters.ts | 50 - src/modules/RootMutations.ts | 61 - src/modules/contracts/ActionContext.ts | 6 - src/modules/contracts/Actions.ts | 6 - src/modules/contracts/Getters.ts | 6 - src/modules/contracts/Module.ts | 13 - src/modules/contracts/RootActionContext.ts | 6 - src/modules/contracts/RootActions.ts | 6 - src/modules/contracts/RootGetters.ts | 6 - src/modules/contracts/RootMutations.ts | 6 - src/modules/contracts/RootState.ts | 5 - src/modules/contracts/State.ts | 10 - src/modules/options/Actions.ts | 13 - src/modules/payloads/Actions.ts | 38 - src/modules/payloads/RootActions.ts | 44 - src/modules/payloads/RootMutations.ts | 41 - src/modules/support/OptionsBuilder.ts | 15 - src/modules/support/PayloadBuilder.ts | 44 - src/plugins/use.ts | 112 -- src/polyfills/index.ts | 6 - src/query/Query.ts | 1441 ----------------- src/query/contracts/GlobalHook.ts | 8 - src/query/contracts/GlobalHooks.ts | 7 - src/query/contracts/HookableClosure.ts | 6 - src/query/contracts/MutationHook.ts | 9 - src/query/contracts/Predicate.ts | 5 - src/query/contracts/RelationshipConstraint.ts | 5 - src/query/contracts/SelectHook.ts | 8 - src/query/contracts/index.ts | 17 - src/query/filters/Filter.ts | 38 - src/query/filters/LimitFilter.ts | 18 - src/query/filters/OrderByFilter.ts | 23 - src/query/filters/WhereFilter.ts | 98 -- src/query/loaders/Loader.ts | 152 -- src/query/options/Constraint.ts | 5 - src/query/options/Has.ts | 11 - src/query/options/HasConstraint.ts | 5 - src/query/options/Load.ts | 7 - src/query/options/OrderDirection.ts | 3 - src/query/options/OrderKey.ts | 5 - src/query/options/Orders.ts | 10 - src/query/options/PersistMethods.ts | 3 - src/query/options/PersistOptions.ts | 8 - src/query/options/PrimaryKey.ts | 3 - src/query/options/Where.ts | 11 - src/query/options/WhereBoolean.ts | 3 - src/query/options/WherePrimaryClosure.ts | 11 - src/query/options/WhereSecondaryClosure.ts | 3 - src/query/options/index.ts | 31 - src/query/processors/Attacher.ts | 27 - src/query/processors/Normalizer.ts | 22 - src/query/processors/PivotCreator.ts | 25 - src/query/processors/Processor.ts | 27 - src/query/rollcallers/Rollcaller.ts | 202 --- src/repository/Repository.ts | 188 --- src/repository/support/Payloads.ts | 39 - src/schema/IdAttribute.ts | 51 - src/schema/Schema.ts | 95 -- src/schema/Schemas.ts | 7 - src/store/index.ts | 94 -- src/support/Uid.ts | 27 - src/support/Utils.ts | 285 ---- src/types/index.ts | 1 - src/types/vuex.ts | 32 - test/Helpers.ts | 1 + test/feature/aggregates/Aggregates.spec.ts | 90 - test/feature/attributes/Boolean.spec.ts | 120 -- test/feature/attributes/Number.spec.ts | 112 -- test/feature/attributes/String.spec.ts | 102 -- test/feature/attributes/Uid.spec.ts | 95 -- test/feature/basics/Delete.spec.ts | 225 --- .../basics/Delete_Composite_Key.spec.ts | 59 - test/feature/basics/Exists.spec.ts | 45 - test/feature/basics/InsertOrUpdate.spec.ts | 70 - test/feature/basics/PersistOptions.spec.ts | 131 -- test/feature/basics/Retrieve.spec.ts | 210 --- .../basics/Retrieve_LimitAndOffset.spec.ts | 53 - test/feature/basics/Retrieve_OrderBy.spec.ts | 145 -- .../basics/Retrieve_SSRSupport.spec.ts | 50 - test/feature/basics/Retrieve_Where.spec.ts | 519 ------ .../Retrieve_Where_Composite_Key.spec.ts | 178 -- test/feature/basics/State.spec.ts | 98 -- test/feature/basics/Update.spec.ts | 690 -------- test/feature/database/RegisterModel.spec.ts | 198 --- test/feature/hooks/Global.spec.ts | 107 -- test/feature/hooks/Global_Create.spec.ts | 94 -- test/feature/hooks/Global_Delete.spec.ts | 74 - test/feature/hooks/Global_Select.spec.ts | 55 - test/feature/hooks/Global_Update.spec.ts | 73 - test/feature/hooks/Local_Binding.spec.ts | 43 - test/feature/hooks/Local_Create.spec.ts | 147 -- test/feature/hooks/Local_Delete.spec.ts | 120 -- test/feature/hooks/Local_Insert.spec.ts | 149 -- test/feature/hooks/Local_Select.spec.ts | 43 - test/feature/hooks/Local_Update.spec.ts | 161 -- test/feature/inheritance/Inheritance.spec.ts | 130 -- .../inheritance/Inheritance_CRUD.spec.ts | 233 --- .../Inheritance_Discriminator.spec.ts | 200 --- .../Inheritance_RelationInstantiation.spec.ts | 174 -- .../inheritance/Inheritance_Relations.spec.ts | 433 ----- test/feature/models/All.spec.ts | 55 - test/feature/models/Create.spec.ts | 257 --- test/feature/models/Find.spec.ts | 288 ---- test/feature/models/Insert.spec.ts | 263 --- test/feature/models/InsertOrUpdate.spec.ts | 207 --- test/feature/models/Query.spec.ts | 52 - test/feature/models/Save.spec.ts | 90 - test/feature/models/Update.spec.ts | 243 --- .../models/static_inserts_create.spec.ts | 102 -- .../models/static_inserts_insert.spec.ts | 103 -- .../feature/models/static_inserts_new.spec.ts | 50 - .../SubGetters_DirectEntityCall.spec.ts | 28 - .../modules/api_inserts_create.spec.ts | 95 -- .../modules/api_inserts_insert.spec.ts | 96 -- test/feature/modules/api_inserts_new.spec.ts | 50 - test/feature/modules/modules.spec.ts | 48 - test/feature/plugin/Plugin.spec.ts | 187 --- test/feature/query/inserts_create.spec.ts | 160 -- test/feature/query/inserts_insert.spec.ts | 160 -- test/feature/query/inserts_new.spec.ts | 50 - test/feature/relations/BelongsTo.spec.ts | 261 --- .../relations/BelongsToMany_Persist.spec.ts | 564 ------- .../relations/BelongsToMany_Retrieve.spec.ts | 453 ------ test/feature/relations/HasMany.spec.ts | 190 --- test/feature/relations/HasManyBy.spec.ts | 284 ---- test/feature/relations/HasManyThrough.spec.ts | 572 ------- test/feature/relations/HasOne.spec.ts | 193 --- test/feature/relations/HasOneThrough.spec.ts | 551 ------- .../relations/MorphMany_Persist.spec.ts | 749 --------- .../relations/MorphMany_Retrieve.spec.ts | 448 ----- .../relations/MorphOne_Persist.spec.ts | 515 ------ .../relations/MorphOne_Retrieve.spec.ts | 404 ----- test/feature/relations/MorphTo.spec.ts | 270 --- .../relations/MorphToMany_Persist.spec.ts | 576 ------- .../relations/MorphToMany_Retrieve.spec.ts | 664 -------- .../relations/MorphedByMany_Persist.spec.ts | 353 ---- .../relations/MorphedByMany_Retrieve.spec.ts | 541 ------- .../relations/Persist_NestedMorph.spec.ts | 81 - .../relations/Retrieve_Constraint.spec.ts | 54 - .../relations/Retrieve_DeepLoad.spec.ts | 88 - test/feature/relations/Retrieve_Has.spec.ts | 321 ---- .../relations/Retrieve_SelfRelation.spec.ts | 39 - .../relations/Retrieve_StringDefine.spec.ts | 56 - test/feature/relations/Retrieve_With.spec.ts | 951 ----------- .../Retrieve_With_NestedConstraints.spec.ts | 149 -- test/feature/relations/WhereFk.spec.ts | 48 - test/feature/repositories/Crud_Delete.spec.ts | 74 - .../repositories/Crud_InsertOrUpdate.spec.ts | 39 - .../repositories/Crud_Retrieve.spec.ts | 195 --- test/feature/repositories/Crud_Update.spec.ts | 35 - test/feature/repositories/Repository.spec.ts | 100 -- test/performance/Retrieve_BelongsTo.spec.ts | 61 - .../Retrieve_BelongsToMany.spec.ts | 79 - test/performance/Retrieve_HasMany.spec.ts | 55 - .../Retrieve_HasManyThrough.spec.ts | 80 - .../Retrieve_HasOneThrough.spec.ts | 76 - test/support/Helpers.ts | 97 -- test/unit/VuexORM.spec.ts | 76 - test/unit/database/Database.spec.ts | 264 --- test/unit/model/Model.spec.ts | 298 ---- .../model/Model_Attributes_Boolean.spec.ts | 95 -- .../model/Model_Attributes_Mutators.spec.ts | 70 - .../model/Model_Attributes_Number.spec.ts | 89 - .../model/Model_Attributes_String.spec.ts | 86 - test/unit/model/Model_Relations.spec.ts | 285 ---- .../Model_Relations_BelongsToMany.spec.ts | 61 - .../model/Model_Relations_MorphMany.spec.ts | 71 - .../model/Model_Relations_MorphOne.spec.ts | 66 - .../model/Model_Relations_MorphTo.spec.ts | 116 -- .../model/Model_Relations_MorphToMany.spec.ts | 112 -- .../Model_Relations_MorphedByMany.spec.ts | 112 -- test/unit/model/Model_Serialization.spec.ts | 138 -- test/unit/model/Model_Store.spec.ts | 174 -- test/unit/model/Model_Utilities.spec.ts | 99 -- test/unit/query/Query.spec.ts | 38 - test/unit/support/Utils.spec.ts | 138 -- 229 files changed, 3 insertions(+), 29137 deletions(-) delete mode 100644 src/attributes/Attribute.ts delete mode 100644 src/attributes/contracts/DictionaryMany.ts delete mode 100644 src/attributes/contracts/DictionaryOne.ts delete mode 100644 src/attributes/contracts/Mutator.ts delete mode 100644 src/attributes/contracts/Mutators.ts delete mode 100644 src/attributes/index.ts delete mode 100644 src/attributes/relations/BelongsTo.ts delete mode 100644 src/attributes/relations/BelongsToMany.ts delete mode 100644 src/attributes/relations/HasMany.ts delete mode 100644 src/attributes/relations/HasManyBy.ts delete mode 100644 src/attributes/relations/HasManyThrough.ts delete mode 100644 src/attributes/relations/HasOne.ts delete mode 100644 src/attributes/relations/HasOneThrough.ts delete mode 100644 src/attributes/relations/MorphMany.ts delete mode 100644 src/attributes/relations/MorphOne.ts delete mode 100644 src/attributes/relations/MorphTo.ts delete mode 100644 src/attributes/relations/MorphToMany.ts delete mode 100644 src/attributes/relations/MorphedByMany.ts delete mode 100644 src/attributes/relations/Relation.ts delete mode 100644 src/attributes/types/Attr.ts delete mode 100644 src/attributes/types/Boolean.ts delete mode 100644 src/attributes/types/Number.ts delete mode 100644 src/attributes/types/String.ts delete mode 100644 src/attributes/types/Type.ts delete mode 100644 src/attributes/types/Uid.ts delete mode 100644 src/connections/Connection.ts delete mode 100644 src/container/Container.ts delete mode 100644 src/data/Collection.ts delete mode 100644 src/data/Collections.ts delete mode 100644 src/data/Entities.ts delete mode 100644 src/data/Instance.ts delete mode 100644 src/data/InstanceOf.ts delete mode 100644 src/data/Instances.ts delete mode 100644 src/data/Item.ts delete mode 100644 src/data/NormalizedData.ts delete mode 100644 src/data/Record.ts delete mode 100644 src/data/RecordWithId.ts delete mode 100644 src/data/Records.ts delete mode 100644 src/data/index.ts delete mode 100644 src/database/Database.ts delete mode 100644 src/database/HackedDatabase.ts delete mode 100644 src/model/Model.ts delete mode 100644 src/model/Serialize.ts delete mode 100644 src/model/contracts/FieldCache.ts delete mode 100644 src/model/contracts/Fields.ts delete mode 100644 src/model/contracts/InheritanceTypes.ts delete mode 100644 src/model/contracts/State.ts delete mode 100644 src/model/support/Payloads.ts delete mode 100644 src/modules/Actions.ts delete mode 100644 src/modules/Getters.ts delete mode 100644 src/modules/RootActions.ts delete mode 100644 src/modules/RootGetters.ts delete mode 100644 src/modules/RootMutations.ts delete mode 100644 src/modules/contracts/ActionContext.ts delete mode 100644 src/modules/contracts/Actions.ts delete mode 100644 src/modules/contracts/Getters.ts delete mode 100644 src/modules/contracts/Module.ts delete mode 100644 src/modules/contracts/RootActionContext.ts delete mode 100644 src/modules/contracts/RootActions.ts delete mode 100644 src/modules/contracts/RootGetters.ts delete mode 100644 src/modules/contracts/RootMutations.ts delete mode 100644 src/modules/contracts/RootState.ts delete mode 100644 src/modules/contracts/State.ts delete mode 100644 src/modules/options/Actions.ts delete mode 100644 src/modules/payloads/Actions.ts delete mode 100644 src/modules/payloads/RootActions.ts delete mode 100644 src/modules/payloads/RootMutations.ts delete mode 100644 src/modules/support/OptionsBuilder.ts delete mode 100644 src/modules/support/PayloadBuilder.ts delete mode 100644 src/plugins/use.ts delete mode 100644 src/polyfills/index.ts delete mode 100644 src/query/Query.ts delete mode 100644 src/query/contracts/GlobalHook.ts delete mode 100644 src/query/contracts/GlobalHooks.ts delete mode 100644 src/query/contracts/HookableClosure.ts delete mode 100644 src/query/contracts/MutationHook.ts delete mode 100644 src/query/contracts/Predicate.ts delete mode 100644 src/query/contracts/RelationshipConstraint.ts delete mode 100644 src/query/contracts/SelectHook.ts delete mode 100644 src/query/contracts/index.ts delete mode 100644 src/query/filters/Filter.ts delete mode 100644 src/query/filters/LimitFilter.ts delete mode 100644 src/query/filters/OrderByFilter.ts delete mode 100644 src/query/filters/WhereFilter.ts delete mode 100644 src/query/loaders/Loader.ts delete mode 100644 src/query/options/Constraint.ts delete mode 100644 src/query/options/Has.ts delete mode 100644 src/query/options/HasConstraint.ts delete mode 100644 src/query/options/Load.ts delete mode 100644 src/query/options/OrderDirection.ts delete mode 100644 src/query/options/OrderKey.ts delete mode 100644 src/query/options/Orders.ts delete mode 100644 src/query/options/PersistMethods.ts delete mode 100644 src/query/options/PersistOptions.ts delete mode 100644 src/query/options/PrimaryKey.ts delete mode 100644 src/query/options/Where.ts delete mode 100644 src/query/options/WhereBoolean.ts delete mode 100644 src/query/options/WherePrimaryClosure.ts delete mode 100644 src/query/options/WhereSecondaryClosure.ts delete mode 100644 src/query/options/index.ts delete mode 100644 src/query/processors/Attacher.ts delete mode 100644 src/query/processors/Normalizer.ts delete mode 100644 src/query/processors/PivotCreator.ts delete mode 100644 src/query/processors/Processor.ts delete mode 100644 src/query/rollcallers/Rollcaller.ts delete mode 100644 src/repository/Repository.ts delete mode 100644 src/repository/support/Payloads.ts delete mode 100644 src/schema/IdAttribute.ts delete mode 100644 src/schema/Schema.ts delete mode 100644 src/schema/Schemas.ts delete mode 100644 src/store/index.ts delete mode 100644 src/support/Uid.ts delete mode 100644 src/support/Utils.ts delete mode 100644 src/types/index.ts delete mode 100644 src/types/vuex.ts create mode 100644 test/Helpers.ts delete mode 100644 test/feature/aggregates/Aggregates.spec.ts delete mode 100644 test/feature/attributes/Boolean.spec.ts delete mode 100644 test/feature/attributes/Number.spec.ts delete mode 100644 test/feature/attributes/String.spec.ts delete mode 100644 test/feature/attributes/Uid.spec.ts delete mode 100644 test/feature/basics/Delete.spec.ts delete mode 100644 test/feature/basics/Delete_Composite_Key.spec.ts delete mode 100644 test/feature/basics/Exists.spec.ts delete mode 100644 test/feature/basics/InsertOrUpdate.spec.ts delete mode 100644 test/feature/basics/PersistOptions.spec.ts delete mode 100644 test/feature/basics/Retrieve.spec.ts delete mode 100644 test/feature/basics/Retrieve_LimitAndOffset.spec.ts delete mode 100644 test/feature/basics/Retrieve_OrderBy.spec.ts delete mode 100644 test/feature/basics/Retrieve_SSRSupport.spec.ts delete mode 100644 test/feature/basics/Retrieve_Where.spec.ts delete mode 100644 test/feature/basics/Retrieve_Where_Composite_Key.spec.ts delete mode 100644 test/feature/basics/State.spec.ts delete mode 100644 test/feature/basics/Update.spec.ts delete mode 100644 test/feature/database/RegisterModel.spec.ts delete mode 100644 test/feature/hooks/Global.spec.ts delete mode 100644 test/feature/hooks/Global_Create.spec.ts delete mode 100644 test/feature/hooks/Global_Delete.spec.ts delete mode 100644 test/feature/hooks/Global_Select.spec.ts delete mode 100644 test/feature/hooks/Global_Update.spec.ts delete mode 100644 test/feature/hooks/Local_Binding.spec.ts delete mode 100644 test/feature/hooks/Local_Create.spec.ts delete mode 100644 test/feature/hooks/Local_Delete.spec.ts delete mode 100644 test/feature/hooks/Local_Insert.spec.ts delete mode 100644 test/feature/hooks/Local_Select.spec.ts delete mode 100644 test/feature/hooks/Local_Update.spec.ts delete mode 100644 test/feature/inheritance/Inheritance.spec.ts delete mode 100644 test/feature/inheritance/Inheritance_CRUD.spec.ts delete mode 100644 test/feature/inheritance/Inheritance_Discriminator.spec.ts delete mode 100644 test/feature/inheritance/Inheritance_RelationInstantiation.spec.ts delete mode 100644 test/feature/inheritance/Inheritance_Relations.spec.ts delete mode 100644 test/feature/models/All.spec.ts delete mode 100644 test/feature/models/Create.spec.ts delete mode 100644 test/feature/models/Find.spec.ts delete mode 100644 test/feature/models/Insert.spec.ts delete mode 100644 test/feature/models/InsertOrUpdate.spec.ts delete mode 100644 test/feature/models/Query.spec.ts delete mode 100644 test/feature/models/Save.spec.ts delete mode 100644 test/feature/models/Update.spec.ts delete mode 100644 test/feature/models/static_inserts_create.spec.ts delete mode 100644 test/feature/models/static_inserts_insert.spec.ts delete mode 100644 test/feature/models/static_inserts_new.spec.ts delete mode 100644 test/feature/modules/SubGetters_DirectEntityCall.spec.ts delete mode 100644 test/feature/modules/api_inserts_create.spec.ts delete mode 100644 test/feature/modules/api_inserts_insert.spec.ts delete mode 100644 test/feature/modules/api_inserts_new.spec.ts delete mode 100644 test/feature/modules/modules.spec.ts delete mode 100644 test/feature/plugin/Plugin.spec.ts delete mode 100644 test/feature/query/inserts_create.spec.ts delete mode 100644 test/feature/query/inserts_insert.spec.ts delete mode 100644 test/feature/query/inserts_new.spec.ts delete mode 100644 test/feature/relations/BelongsTo.spec.ts delete mode 100644 test/feature/relations/BelongsToMany_Persist.spec.ts delete mode 100644 test/feature/relations/BelongsToMany_Retrieve.spec.ts delete mode 100644 test/feature/relations/HasMany.spec.ts delete mode 100644 test/feature/relations/HasManyBy.spec.ts delete mode 100644 test/feature/relations/HasManyThrough.spec.ts delete mode 100644 test/feature/relations/HasOne.spec.ts delete mode 100644 test/feature/relations/HasOneThrough.spec.ts delete mode 100644 test/feature/relations/MorphMany_Persist.spec.ts delete mode 100644 test/feature/relations/MorphMany_Retrieve.spec.ts delete mode 100644 test/feature/relations/MorphOne_Persist.spec.ts delete mode 100644 test/feature/relations/MorphOne_Retrieve.spec.ts delete mode 100644 test/feature/relations/MorphTo.spec.ts delete mode 100644 test/feature/relations/MorphToMany_Persist.spec.ts delete mode 100644 test/feature/relations/MorphToMany_Retrieve.spec.ts delete mode 100644 test/feature/relations/MorphedByMany_Persist.spec.ts delete mode 100644 test/feature/relations/MorphedByMany_Retrieve.spec.ts delete mode 100644 test/feature/relations/Persist_NestedMorph.spec.ts delete mode 100644 test/feature/relations/Retrieve_Constraint.spec.ts delete mode 100644 test/feature/relations/Retrieve_DeepLoad.spec.ts delete mode 100644 test/feature/relations/Retrieve_Has.spec.ts delete mode 100644 test/feature/relations/Retrieve_SelfRelation.spec.ts delete mode 100644 test/feature/relations/Retrieve_StringDefine.spec.ts delete mode 100644 test/feature/relations/Retrieve_With.spec.ts delete mode 100644 test/feature/relations/Retrieve_With_NestedConstraints.spec.ts delete mode 100644 test/feature/relations/WhereFk.spec.ts delete mode 100644 test/feature/repositories/Crud_Delete.spec.ts delete mode 100644 test/feature/repositories/Crud_InsertOrUpdate.spec.ts delete mode 100644 test/feature/repositories/Crud_Retrieve.spec.ts delete mode 100644 test/feature/repositories/Crud_Update.spec.ts delete mode 100644 test/feature/repositories/Repository.spec.ts delete mode 100644 test/performance/Retrieve_BelongsTo.spec.ts delete mode 100644 test/performance/Retrieve_BelongsToMany.spec.ts delete mode 100644 test/performance/Retrieve_HasMany.spec.ts delete mode 100644 test/performance/Retrieve_HasManyThrough.spec.ts delete mode 100644 test/performance/Retrieve_HasOneThrough.spec.ts delete mode 100644 test/support/Helpers.ts delete mode 100644 test/unit/VuexORM.spec.ts delete mode 100644 test/unit/database/Database.spec.ts delete mode 100644 test/unit/model/Model.spec.ts delete mode 100644 test/unit/model/Model_Attributes_Boolean.spec.ts delete mode 100644 test/unit/model/Model_Attributes_Mutators.spec.ts delete mode 100644 test/unit/model/Model_Attributes_Number.spec.ts delete mode 100644 test/unit/model/Model_Attributes_String.spec.ts delete mode 100644 test/unit/model/Model_Relations.spec.ts delete mode 100644 test/unit/model/Model_Relations_BelongsToMany.spec.ts delete mode 100644 test/unit/model/Model_Relations_MorphMany.spec.ts delete mode 100644 test/unit/model/Model_Relations_MorphOne.spec.ts delete mode 100644 test/unit/model/Model_Relations_MorphTo.spec.ts delete mode 100644 test/unit/model/Model_Relations_MorphToMany.spec.ts delete mode 100644 test/unit/model/Model_Relations_MorphedByMany.spec.ts delete mode 100644 test/unit/model/Model_Serialization.spec.ts delete mode 100644 test/unit/model/Model_Store.spec.ts delete mode 100644 test/unit/model/Model_Utilities.spec.ts delete mode 100644 test/unit/query/Query.spec.ts delete mode 100644 test/unit/support/Utils.spec.ts diff --git a/src/attributes/Attribute.ts b/src/attributes/Attribute.ts deleted file mode 100644 index 866f62d4..00000000 --- a/src/attributes/Attribute.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Record from '../data/Record' -import Model from '../model/Model' - -export default abstract class Attribute { - /** - * The model that this attributes is being registerd. - */ - model: typeof Model - - /** - * The default value of the field. - */ - value: any - - /** - * Create a new attribute instance. - */ - constructor(model: typeof Model) { - this.model = model - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - abstract make(value: any, parent: Record, key: string): any -} diff --git a/src/attributes/contracts/DictionaryMany.ts b/src/attributes/contracts/DictionaryMany.ts deleted file mode 100644 index 3ff747da..00000000 --- a/src/attributes/contracts/DictionaryMany.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Collection from '../../data/Collection' - -export interface DictionaryMany { - [key: string]: Collection -} - -export default DictionaryMany diff --git a/src/attributes/contracts/DictionaryOne.ts b/src/attributes/contracts/DictionaryOne.ts deleted file mode 100644 index fba70b9f..00000000 --- a/src/attributes/contracts/DictionaryOne.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Model from '../../model/Model' - -export interface DictionaryOne { - [key: string]: Model -} - -export default DictionaryOne diff --git a/src/attributes/contracts/Mutator.ts b/src/attributes/contracts/Mutator.ts deleted file mode 100644 index 44a5350d..00000000 --- a/src/attributes/contracts/Mutator.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type Mutator = (value: T) => T - -export default Mutator diff --git a/src/attributes/contracts/Mutators.ts b/src/attributes/contracts/Mutators.ts deleted file mode 100644 index 004f5c61..00000000 --- a/src/attributes/contracts/Mutators.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Mutator from './Mutator' - -export interface Mutators { - [name: string]: Mutator -} - -export default Mutators diff --git a/src/attributes/index.ts b/src/attributes/index.ts deleted file mode 100644 index fc70103d..00000000 --- a/src/attributes/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Attribute from './Attribute' -import Type from './types/Type' -import Attr from './types/Attr' -import String from './types/String' -import Number from './types/Number' -import Boolean from './types/Boolean' -import Uid from './types/Uid' -import Relation from './relations/Relation' -import HasOne from './relations/HasOne' -import BelongsTo from './relations/BelongsTo' -import HasMany from './relations/HasMany' -import HasManyBy from './relations/HasManyBy' -import HasOneThrough from './relations/HasOneThrough' -import HasManyThrough from './relations/HasManyThrough' -import BelongsToMany from './relations/BelongsToMany' -import MorphTo from './relations/MorphTo' -import MorphOne from './relations/MorphOne' -import MorphMany from './relations/MorphMany' -import MorphToMany from './relations/MorphToMany' -import MorphedByMany from './relations/MorphedByMany' - -export { - Attribute, - Type, - Attr, - String, - Number, - Boolean, - Uid, - Relation, - HasOne, - BelongsTo, - HasMany, - HasManyBy, - HasOneThrough, - HasManyThrough, - BelongsToMany, - MorphTo, - MorphOne, - MorphMany, - MorphToMany, - MorphedByMany -} diff --git a/src/attributes/relations/BelongsTo.ts b/src/attributes/relations/BelongsTo.ts deleted file mode 100644 index d8191e4f..00000000 --- a/src/attributes/relations/BelongsTo.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import DictionaryOne from '../contracts/DictionaryOne' -import Relation from './Relation' - -export default class BelongsTo extends Relation { - /** - * The parent model. - */ - parent: typeof Model - - /** - * The foreign key of the model. - */ - foreignKey: string - - /** - * The associated key on the parent model. - */ - ownerKey: string - - /** - * Create a new belongs to instance. - */ - constructor( - model: typeof Model, - parent: typeof Model | string, - foreignKey: string, - ownerKey: string - ) { - super(model) /* istanbul ignore next */ - - this.parent = this.model.relation(parent) - this.foreignKey = foreignKey - this.ownerKey = ownerKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.one(this.parent) - } - - /** - * Attach the relational key to the given data. For example, when Post - * belongs to User, it will attach value to the `user_id` field of - * Post record. - */ - attach(key: any, record: Record, data: NormalizedData): void { - // See if the record has the foreign key, if yes, it means the user has - // provided the key explicitly so do nothing and return. - if (record[this.foreignKey] !== undefined) { - return - } - - // If there is no foreign key, let's set it here. - record[this.foreignKey] = - data[this.parent.entity] && data[this.parent.entity][key] - ? data[this.parent.entity][key][this.ownerKey] - : key - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model | null { - return this.makeOneRelation(value, this.parent) - } - - /** - * Load the belongs to relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relation = this.getRelation(query, this.parent.entity, constraints) - - this.addEagerConstraints(relation, collection) - - this.match(collection, relation.get(), name) - } - - /** - * Set the constraints for an eager load of the relation. - */ - private addEagerConstraints(relation: Query, collection: Collection): void { - relation.whereFk(this.ownerKey, this.getKeys(collection, this.foreignKey)) - } - - /** - * Match the eagerly loaded results to their parents. - */ - private match( - collection: Collection, - relations: Collection, - name: string - ): void { - const dictionary = this.buildDictionary(relations) - - collection.forEach((model) => { - const id = model[this.foreignKey] - const relation = id !== null ? dictionary[id] : null - - model[name] = relation || null - }) - } - - /** - * Build model dictionary keyed by the relation's foreign key. - */ - private buildDictionary(relations: Collection): DictionaryOne { - return relations.reduce((dictionary, relation) => { - const key = relation[this.ownerKey] - - dictionary[key] = relation - - return dictionary - }, {}) - } -} diff --git a/src/attributes/relations/BelongsToMany.ts b/src/attributes/relations/BelongsToMany.ts deleted file mode 100644 index 9858a518..00000000 --- a/src/attributes/relations/BelongsToMany.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import Utils from '../../support/Utils' -import { Record, Records, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export interface PivotRecord { - [entity: string]: { - [id: string]: { - $id: string - [pivotKey: string]: any - } - } -} - -export default class BelongsToMany extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The pivot model. - */ - pivot: typeof Model - - /** - * The foreign key of the parent model. - */ - foreignPivotKey: string - - /** - * The associated key of the relation. - */ - relatedPivotKey: string - - /** - * The key name of the parent model. - */ - parentKey: string - - /** - * The key name of the related model. - */ - relatedKey: string - - /** - * The key name of the pivot data. - */ - pivotKey: string = 'pivot' - - /** - * Create a new belongs to instance. - */ - constructor( - model: typeof Model, - related: Entity, - pivot: Entity, - foreignPivotKey: string, - relatedPivotKey: string, - parentKey: string, - relatedKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.pivot = this.model.relation(pivot) - this.foreignPivotKey = foreignPivotKey - this.relatedPivotKey = relatedPivotKey - this.parentKey = parentKey - this.relatedKey = relatedKey - } - - /** - * Specify the custom pivot accessor to use for the relationship. - */ - as(accessor: string): this { - this.pivotKey = accessor - - return this - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.related) - } - - /** - * Attach the relational key to the given data. Since belongs to many - * relationship doesn't have any foreign key, it would do nothing. - */ - attach(_key: any, _record: Record, _data: NormalizedData): void { - return - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.related) - } - - /** - * Load the belongs to relationship for the record. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - const pivotQuery = query.newQuery(this.pivot.entity) - - this.addEagerConstraintForPivot(pivotQuery, collection) - - const pivots = pivotQuery.get() - - this.addEagerConstraintForRelated(relatedQuery, pivots) - - const relateds = this.mapPivotRelations(pivots, relatedQuery) - - collection.forEach((item) => { - const related = relateds[item[this.parentKey]] - - item[name] = related || [] - }) - } - - /** - * Set the constraints for the pivot relation. - */ - addEagerConstraintForPivot(query: Query, collection: Collection): void { - query.whereFk( - this.foreignPivotKey, - this.getKeys(collection, this.parentKey) - ) - } - - /** - * Set the constraints for the related relation. - */ - addEagerConstraintForRelated(query: Query, collection: Collection): void { - query.whereFk( - this.relatedKey, - this.getKeys(collection, this.relatedPivotKey) - ) - } - - /** - * Create a new indexed map for the pivot relation. - */ - mapPivotRelations(pivots: Collection, relatedQuery: Query): Records { - const relations = this.mapManyRelations(relatedQuery.get(), this.relatedKey) - - if (relatedQuery.orders.length) { - return this.mapRelationsByOrders( - pivots, - relations, - this.foreignPivotKey, - this.relatedPivotKey - ) - } - - return pivots.reduce((records, record) => { - const id = record[this.foreignPivotKey] - - if (!records[id]) { - records[id] = [] - } - - const related = relations.get(record[this.relatedPivotKey]) - - if (related) { - records[id] = records[id].concat( - related.map((model: Record) => { - model[this.pivotKey] = record - return model - }) - ) - } - - return records - }, {}) - } - - /** - * Create pivot records for the given records if needed. - */ - createPivots( - parent: typeof Model, - data: NormalizedData, - key: string - ): NormalizedData { - if (!Utils.isArray(this.pivot.primaryKey)) return data - - Utils.forOwn(data[parent.entity], (record) => { - const related = record[key] - - if (related === undefined || related.length === 0) { - return - } - - this.createPivotRecord(data, record, related) - }) - - return data - } - - /** - * Create a pivot record. - */ - createPivotRecord( - data: NormalizedData, - record: Record, - related: any[] - ): void { - related.forEach((id) => { - const parentId = record[this.parentKey] - const relatedId = data[this.related.entity][id][this.relatedKey] - const pivotKey = JSON.stringify([ - this.pivot.primaryKey[0] === this.foreignPivotKey - ? parentId - : relatedId, - this.pivot.primaryKey[1] === this.foreignPivotKey ? parentId : relatedId - ]) - const pivotRecord = data[this.pivot.entity] - ? data[this.pivot.entity][pivotKey] - : {} - const pivotData = data[this.related.entity][id][this.pivotKey] || {} - - data[this.pivot.entity] = { - ...data[this.pivot.entity], - - [pivotKey]: { - ...pivotRecord, - ...pivotData, - $id: pivotKey, - [this.foreignPivotKey]: parentId, - [this.relatedPivotKey]: relatedId - } - } - }) - } -} diff --git a/src/attributes/relations/HasMany.ts b/src/attributes/relations/HasMany.ts deleted file mode 100644 index 8e8b9244..00000000 --- a/src/attributes/relations/HasMany.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import DictionaryMany from '../contracts/DictionaryMany' -import Relation from './Relation' - -export default class HasMany extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The foreign key of the related model. - */ - foreignKey: string - - /** - * The local key of the model. - */ - localKey: string - - /** - * Create a new has many instance. - */ - constructor( - model: typeof Model, - related: typeof Model | string, - foreignKey: string, - localKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.foreignKey = foreignKey - this.localKey = localKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.related) - } - - /** - * Attach the relational key to the given data. - */ - attach(key: any, record: Record, data: NormalizedData): void { - key.forEach((index: any) => { - const related = data[this.related.entity] - - if ( - !related || - !related[index] || - related[index][this.foreignKey] !== undefined - ) { - return - } - - related[index][this.foreignKey] = record[this.localKey] - }) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.related) - } - - /** - * Load the has many relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relation = this.getRelation(query, this.related.entity, constraints) - - this.addEagerConstraints(relation, collection) - - this.match(collection, relation.get(), name) - } - - /** - * Set the constraints for an eager load of the relation. - */ - private addEagerConstraints(relation: Query, collection: Collection): void { - relation.whereFk(this.foreignKey, this.getKeys(collection, this.localKey)) - } - - /** - * Match the eagerly loaded results to their parents. - */ - private match( - collection: Collection, - relations: Collection, - name: string - ): void { - const dictionary = this.buildDictionary(relations) - - collection.forEach((model) => { - const id = model[this.localKey] - const relation = dictionary[id] - - model[name] = relation || [] - }) - } - - /** - * Build model dictionary keyed by the relation's foreign key. - */ - private buildDictionary(relations: Collection): DictionaryMany { - return relations.reduce((dictionary, relation) => { - const key = relation[this.foreignKey] - - if (!dictionary[key]) { - dictionary[key] = [] - } - - dictionary[key].push(relation) - - return dictionary - }, {}) - } -} diff --git a/src/attributes/relations/HasManyBy.ts b/src/attributes/relations/HasManyBy.ts deleted file mode 100644 index ca06ed28..00000000 --- a/src/attributes/relations/HasManyBy.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export default class HasManyBy extends Relation { - /** - * The related model. - */ - parent: typeof Model - - /** - * The foreign key of the model. - */ - foreignKey: string - - /** - * The associated key on the parent model. - */ - ownerKey: string - - /** - * Create a new has many by instance. - */ - constructor( - model: typeof Model, - parent: typeof Model | string, - foreignKey: string, - ownerKey: string - ) { - super(model) /* istanbul ignore next */ - - this.parent = this.model.relation(parent) - this.foreignKey = foreignKey - this.ownerKey = ownerKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.parent) - } - - /** - * Attach the relational key to the given data. - */ - attach(key: any, record: Record, _data: NormalizedData): void { - if (key.length === 0) { - return - } - - record[this.foreignKey] = key.map((parentId: any) => { - return this.parent.getIdFromRecord(_data[this.parent.entity][parentId]) - }) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.parent) - } - - /** - * Load the has many by relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.parent.entity, - constraints - ) - - this.addConstraintForHasManyBy(relatedQuery, collection) - - const relations = this.mapSingleRelations(relatedQuery.get(), this.ownerKey) - - collection.forEach((item) => { - const related = this.getRelatedRecords(relations, item[this.foreignKey]) - - item[name] = related - }) - } - - /** - * Set the constraints for an eager load of the relation. - */ - addConstraintForHasManyBy(query: Query, collection: Collection): void { - const keys = collection.reduce((keys, item) => { - return keys.concat(item[this.foreignKey]) - }, [] as string[]) - - query.where(this.ownerKey, keys) - } - - /** - * Get related records. - */ - getRelatedRecords(relations: Map, keys: string[]): Record[] { - const records: Record[] = [] - - relations.forEach((record, id) => { - if (keys.indexOf(id) !== -1) { - records.push(record) - } - }) - - return records - } -} diff --git a/src/attributes/relations/HasManyThrough.ts b/src/attributes/relations/HasManyThrough.ts deleted file mode 100644 index 678f28b5..00000000 --- a/src/attributes/relations/HasManyThrough.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, Records, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class HasManyThrough extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The "through" parent model. - */ - through: typeof Model - - /** - * The near key on the relationship. - */ - firstKey: string - - /** - * The far key on the relationship. - */ - secondKey: string - - /** - * The local key on the relationship. - */ - localKey: string - - /** - * The local key on the intermediary model. - */ - secondLocalKey: string - - /** - * Create a new has many through instance. - */ - constructor( - model: typeof Model, - related: Entity, - through: Entity, - firstKey: string, - secondKey: string, - localKey: string, - secondLocalKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.through = this.model.relation(through) - this.firstKey = firstKey - this.secondKey = secondKey - this.localKey = localKey - this.secondLocalKey = secondLocalKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.related) - } - - /** - * Attach the relational key to the given data. Since has many through - * relationship doesn't have any foreign key, it would do nothing. - */ - attach(_key: any, _record: Record, _data: NormalizedData): void { - return - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.related) - } - - /** - * Load the has many through relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - const throughQuery = query.newQuery(this.through.entity) - - this.addEagerConstraintForThrough(throughQuery, collection) - - const throughs = throughQuery.get() - - this.addEagerConstraintForRelated(relatedQuery, throughs) - - const relateds = this.mapThroughRelations(throughs, relatedQuery) - - collection.forEach((item) => { - const related = relateds[item[this.localKey]] - - item[name] = related || [] - }) - } - - /** - * Set the constraints for the through relation. - */ - addEagerConstraintForThrough(query: Query, collection: Collection): void { - query.where(this.firstKey, this.getKeys(collection, this.localKey)) - } - - /** - * Set the constraints for the related relation. - */ - addEagerConstraintForRelated(query: Query, collection: Collection): void { - query.where(this.secondKey, this.getKeys(collection, this.secondLocalKey)) - } - - /** - * Create a new indexed map for the through relation. - */ - mapThroughRelations(throughs: Collection, relatedQuery: Query): Records { - const relations = this.mapManyRelations(relatedQuery.get(), this.secondKey) - - return throughs.reduce((records, record) => { - const id = record[this.firstKey] - - if (!records[id]) { - records[id] = [] - } - - const related = relations.get(record[this.secondLocalKey]) - - if (related === undefined) { - return records - } - - records[id] = records[id].concat(related) - - return records - }, {}) - } -} diff --git a/src/attributes/relations/HasOne.ts b/src/attributes/relations/HasOne.ts deleted file mode 100644 index 550c4ab2..00000000 --- a/src/attributes/relations/HasOne.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import DictionaryOne from '../contracts/DictionaryOne' -import Relation from './Relation' - -export default class HasOne extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The foreign key of the related model. - */ - foreignKey: string - - /** - * The local key of the model. - */ - localKey: string - - /** - * Create a new has one instance. - */ - constructor( - model: typeof Model, - related: typeof Model | string, - foreignKey: string, - localKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.foreignKey = foreignKey - this.localKey = localKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.one(this.related) - } - - /** - * Attach the relational key to the related data. For example, - * when User has one Phone, it will attach value to the - * `user_id` field of Phone record. - */ - attach(key: any, record: Record, data: NormalizedData): void { - // Check if the record has local key set. If not, set the local key to be - // the id value. This happens if the user defines the custom local key - // and didn't include it in the data being normalized. - if (!record[this.localKey]) { - record[this.localKey] = this.model.getIndexIdFromRecord(record) - } - - // Then set the foreign key of the related record if it exists to be the - // local key of this record. - const related = data[this.related.entity] && data[this.related.entity][key] - - if (related) { - related[this.foreignKey] = record[this.localKey] - } - } - - /** - * Make value to be set to model property. This method is used when - * instantiating a model or creating a plain object from a model. - */ - make(value: any, _parent: Record, _key: string): Model | null { - return this.makeOneRelation(value, this.related) - } - - /** - * Load the has one relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relation = this.getRelation(query, this.related.entity, constraints) - - this.addEagerConstraints(relation, collection) - - this.match(collection, relation.get(), name) - } - - /** - * Set the constraints for an eager load of the relation. - */ - private addEagerConstraints(relation: Query, collection: Collection): void { - relation.whereFk(this.foreignKey, this.getKeys(collection, this.localKey)) - } - - /** - * Match the eagerly loaded results to their parents. - */ - private match( - collection: Collection, - relations: Collection, - name: string - ): void { - const dictionary = this.buildDictionary(relations) - - collection.forEach((model) => { - const id = model[this.localKey] - const relation = dictionary[id] - - model[name] = relation || null - }) - } - - /** - * Build model dictionary keyed by the relation's foreign key. - */ - private buildDictionary(relations: Collection): DictionaryOne { - return relations.reduce((dictionary, relation) => { - const key = relation[this.foreignKey] - - dictionary[key] = relation - - return dictionary - }, {}) - } -} diff --git a/src/attributes/relations/HasOneThrough.ts b/src/attributes/relations/HasOneThrough.ts deleted file mode 100644 index 17547306..00000000 --- a/src/attributes/relations/HasOneThrough.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, Records, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class HasOneThrough extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The "through" parent model. - */ - through: typeof Model - - /** - * The near key on the relation. - */ - firstKey: string - - /** - * The far key on the relation. - */ - secondKey: string - - /** - * The local key on the relation. - */ - localKey: string - - /** - * The local key on the intermediary model. - */ - secondLocalKey: string - - /** - * Create a new relation instance. - */ - constructor( - model: typeof Model, - related: Entity, - through: Entity, - firstKey: string, - secondKey: string, - localKey: string, - secondLocalKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.through = this.model.relation(through) - this.firstKey = firstKey - this.secondKey = secondKey - this.localKey = localKey - this.secondLocalKey = secondLocalKey - } - - /** - * Define the normalizr schema for the relation. - */ - define(schema: Schema): NormalizrSchema { - return schema.one(this.related) - } - - /** - * Since the relation doesn't have a foreign key, there is no relational key - * to attach to the given data. - */ - attach(_key: any, _record: Record, _data: NormalizedData): void { - return - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model | null { - return this.makeOneRelation(value, this.related) - } - - /** - * Load the relation for the given collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - const throughQuery = query.newQuery(this.through.entity) - - this.addEagerConstraintForThrough(throughQuery, collection) - - const through = throughQuery.get() - - this.addEagerConstraintForRelated(relatedQuery, through) - - const related = this.mapThroughRelations(through, relatedQuery) - - collection.forEach((item) => { - const relation = related[item[this.localKey]] - - item[name] = relation || null - }) - } - - /** - * Set the constraints for the "through" relation. - */ - addEagerConstraintForThrough(query: Query, collection: Collection): void { - query.where(this.firstKey, this.getKeys(collection, this.localKey)) - } - - /** - * Set the constraints for the "related" relation. - */ - addEagerConstraintForRelated(query: Query, collection: Collection): void { - query.where(this.secondKey, this.getKeys(collection, this.secondLocalKey)) - } - - /** - * Create a new indexed map for the "through" relation. - */ - mapThroughRelations(through: Collection, relatedQuery: Query): Records { - const relations = this.mapSingleRelations( - relatedQuery.get(), - this.secondKey - ) - - return through.reduce((records, record) => { - const id = record[this.firstKey] - - const related = relations.get(record[this.secondLocalKey]) - - records[id] = related || null - - return records - }, {}) - } -} diff --git a/src/attributes/relations/MorphMany.ts b/src/attributes/relations/MorphMany.ts deleted file mode 100644 index 80248806..00000000 --- a/src/attributes/relations/MorphMany.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class MorphMany extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The field name that contains id of the parent model. - */ - id: string - - /** - * The field name that contains type of the parent model. - */ - type: string - - /** - * The local key of the model. - */ - localKey: string - - /** - * Create a new belongs to instance. - */ - constructor( - model: typeof Model, - related: Entity, - id: string, - type: string, - localKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.id = id - this.type = type - this.localKey = localKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.related) - } - - /** - * Attach the relational key to the given data. - */ - attach(key: any, record: Record, data: NormalizedData): void { - const relatedItems = data[this.related.entity] - - key.forEach((id: any) => { - const relatedItem = relatedItems[id] - - relatedItem[this.id] = - relatedItem[this.id] || this.related.getIdFromRecord(record) - relatedItem[this.type] = relatedItem[this.type] || this.model.entity - }) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.related) - } - - /** - * Load the morph many relationship for the record. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - this.addEagerConstraintForMorphMany(relatedQuery, collection, query.entity) - - const relations = this.mapManyRelations(relatedQuery.get(), this.id) - - collection.forEach((item) => { - const related = relations.get(item[this.localKey]) - - item[name] = related || [] - }) - } - - /** - * Set the constraints for an eager load of the relation. - */ - addEagerConstraintForMorphMany( - query: Query, - collection: Collection, - type: string - ): void { - query - .whereFk(this.type, type) - .whereFk(this.id, this.getKeys(collection, this.localKey)) - } -} diff --git a/src/attributes/relations/MorphOne.ts b/src/attributes/relations/MorphOne.ts deleted file mode 100644 index 15671a2a..00000000 --- a/src/attributes/relations/MorphOne.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class MorphOne extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The field name that contains id of the parent model. - */ - id: string - - /** - * The field name that contains type of the parent model. - */ - type: string - - /** - * The local key of the model. - */ - localKey: string - - /** - * Create a new belongs to instance. - */ - constructor( - model: typeof Model, - related: Entity, - id: string, - type: string, - localKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.id = id - this.type = type - this.localKey = localKey - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.one(this.related) - } - - /** - * Attach the relational key to the given data. - */ - attach(key: any, record: Record, data: NormalizedData): void { - const relatedRecord = data[this.related.entity][key] - - relatedRecord[this.id] = - relatedRecord[this.id] || this.related.getIdFromRecord(record) - relatedRecord[this.type] = relatedRecord[this.type] || this.model.entity - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model | null { - return this.makeOneRelation(value, this.related) - } - - /** - * Load the morph many relationship for the record. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - this.addEagerConstraintForMorphOne(relatedQuery, collection, query.entity) - - const relations = this.mapSingleRelations(relatedQuery.get(), this.id) - - collection.forEach((item) => { - const related = relations.get(item[this.localKey]) - - item[name] = related || null - }) - } - - /** - * Set the constraints for an eager load of the relation. - */ - addEagerConstraintForMorphOne( - query: Query, - collection: Collection, - type: string - ): void { - query - .whereFk(this.type, type) - .whereFk(this.id, this.getKeys(collection, this.localKey)) - } -} diff --git a/src/attributes/relations/MorphTo.ts b/src/attributes/relations/MorphTo.ts deleted file mode 100644 index df5a90b7..00000000 --- a/src/attributes/relations/MorphTo.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class MorphTo extends Relation { - /** - * The field name that contains id of the parent model. - */ - id: string - - /** - * The field name that contains type of the parent model. - */ - type: string - - /** - * Create a new morph to instance. - */ - constructor(model: typeof Model, id: string, type: string) { - super(model) /* istanbul ignore next */ - - this.id = id - this.type = type - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.union((_value, parentValue) => parentValue[this.type]) - } - - /** - * Attach the relational key to the given record. Since morph to - * relationship doesn't have any foreign key, it would do nothing. - */ - attach(_key: any, _record: Record, _data: NormalizedData): void { - return - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, parent: Record, _key: string): Model | null { - const related = parent[this.type] as string - - try { - const model = this.model.relation(related) - return this.makeOneRelation(value, model) - } catch { - return null - } - } - - /** - * Load the morph to relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const types = this.getTypes(collection) - - const relations = types.reduce((related, type) => { - const relatedQuery = this.getRelation(query, type, constraints) - - related[type] = this.mapSingleRelations(relatedQuery.get(), '$id') - - return related - }, {}) - - collection.forEach((item) => { - const id = item[this.id] - const type = item[this.type] - const related = relations[type].get(String(id)) - - item[name] = related || null - }) - } - - /** - * Get all types from the collection. - */ - getTypes(collection: Collection): string[] { - return collection.reduce((types, item) => { - const type = item[this.type] - - !types.includes(type) && types.push(type) - - return types - }, [] as string[]) - } -} diff --git a/src/attributes/relations/MorphToMany.ts b/src/attributes/relations/MorphToMany.ts deleted file mode 100644 index a4982c3c..00000000 --- a/src/attributes/relations/MorphToMany.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Utils from '../../support/Utils' -import Schema from '../../schema/Schema' -import { Record, Records, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class MorphToMany extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The pivot model. - */ - pivot: typeof Model - - /** - * The field name that contains id of the related model. - */ - relatedId: string - - /** - * The field name that contains id of the parent model. - */ - id: string - - /** - * The field name that contains type of the parent model. - */ - type: string - - /** - * The key name of the parent model. - */ - parentKey: string - - /** - * The key name of the related model. - */ - relatedKey: string - - /** - * The key name of the pivot data. - */ - pivotKey: string = 'pivot' - - /** - * Create a new belongs to instance. - */ - constructor( - model: typeof Model, - related: Entity, - pivot: Entity, - relatedId: string, - id: string, - type: string, - parentKey: string, - relatedKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.pivot = this.model.relation(pivot) - this.relatedId = relatedId - this.id = id - this.type = type - this.parentKey = parentKey - this.relatedKey = relatedKey - } - - /** - * Specify the custom pivot accessor to use for the relationship. - */ - as(accessor: string): this { - this.pivotKey = accessor - - return this - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.related) - } - - /** - * Attach the relational key to the given record. Since morph to many - * relationship doesn't have any foreign key, it would do nothing. - */ - attach(_key: any, _record: Record, _data: NormalizedData): void { - return - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.related) - } - - /** - * Load the morph to many relationship for the collection. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - const pivotQuery = query.newQuery(this.pivot.entity) - - this.addEagerConstraintForPivot(pivotQuery, collection, query.entity) - - const pivots = pivotQuery.get() - - this.addEagerConstraintForRelated(relatedQuery, pivots) - - const relateds = this.mapPivotRelations(pivots, relatedQuery) - - collection.forEach((item) => { - const related = relateds[item[this.parentKey]] - - item[name] = related || [] - }) - } - - /** - * Set the constraints for the pivot relation. - */ - addEagerConstraintForPivot( - query: Query, - collection: Collection, - type: string - ): void { - query - .whereFk(this.type, type) - .whereFk(this.id, this.getKeys(collection, this.parentKey)) - } - - /** - * Set the constraints for the related relation. - */ - addEagerConstraintForRelated(query: Query, collection: Collection): void { - query.whereFk(this.relatedKey, this.getKeys(collection, this.relatedId)) - } - - /** - * Create a new indexed map for the pivot relation. - */ - mapPivotRelations(pivots: Collection, relatedQuery: Query): Records { - const relations = this.mapManyRelations(relatedQuery.get(), this.relatedKey) - - if (relatedQuery.orders.length) { - return this.mapRelationsByOrders( - pivots, - relations, - this.id, - this.relatedId - ) - } - - return pivots.reduce((records, record) => { - const id = record[this.id] - - if (!records[id]) { - records[id] = [] - } - - const related = relations.get(record[this.relatedId]) - - /* istanbul ignore if */ - if (related === undefined || related.length === 0) { - return records - } - - records[id] = records[id].concat( - related.map((model: Record) => { - model[this.pivotKey] = record - return model - }) - ) - - return records - }, {}) - } - - /** - * Create pivot records for the given records if needed. - */ - createPivots( - parent: typeof Model, - data: NormalizedData, - key: string - ): NormalizedData { - Utils.forOwn(data[parent.entity], (record) => { - const relatedIds = parent - .query() - .newQuery(this.pivot.entity) - .where(this.id, record[this.parentKey]) - .where(this.type, parent.entity) - .get() - - const relateds = (record[key] || []).filter( - (relatedId: any) => !relatedIds.includes(relatedId) - ) - - if (!Utils.isArray(relateds) || relateds.length === 0) { - return - } - - this.createPivotRecord(parent, data, record, relateds) - }) - - return data - } - - /** - * Create a pivot record. - */ - createPivotRecord( - parent: typeof Model, - data: NormalizedData, - record: Record, - related: any[] - ): void { - related.forEach((id) => { - const parentId = record[this.parentKey] - const relatedId = data[this.related.entity][id][this.relatedKey] - const pivotKey = `${parentId}_${id}_${parent.entity}` - const pivotData = data[this.related.entity][id][this.pivotKey] || {} - - data[this.pivot.entity] = { - ...data[this.pivot.entity], - - [pivotKey]: { - ...pivotData, - $id: pivotKey, - [this.relatedId]: relatedId, - [this.id]: parentId, - [this.type]: parent.entity - } - } - }) - } -} diff --git a/src/attributes/relations/MorphedByMany.ts b/src/attributes/relations/MorphedByMany.ts deleted file mode 100644 index 3e78c2f2..00000000 --- a/src/attributes/relations/MorphedByMany.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import Utils from '../../support/Utils' -import Schema from '../../schema/Schema' -import { Record, Records, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Relation from './Relation' - -export type Entity = typeof Model | string - -export default class MorphedByMany extends Relation { - /** - * The related model. - */ - related: typeof Model - - /** - * The pivot model. - */ - pivot: typeof Model - - /** - * The field name that contains id of the related model. - */ - relatedId: string - - /** - * The field name that contains id of the parent model. - */ - id: string - - /** - * The field name that contains type of the parent model. - */ - type: string - - /** - * The key name of the parent model. - */ - parentKey: string - - /** - * The key name of the related model. - */ - relatedKey: string - - /** - * The key name of the pivot data. - */ - pivotKey: string = 'pivot' - - /** - * Create a new belongs to instance. - */ - constructor( - model: typeof Model, - related: Entity, - pivot: Entity, - relatedId: string, - id: string, - type: string, - parentKey: string, - relatedKey: string - ) { - super(model) /* istanbul ignore next */ - - this.related = this.model.relation(related) - this.pivot = this.model.relation(pivot) - this.relatedId = relatedId - this.id = id - this.type = type - this.parentKey = parentKey - this.relatedKey = relatedKey - } - - /** - * Specify the custom pivot accessor to use for the relationship. - */ - as(accessor: string): this { - this.pivotKey = accessor - - return this - } - - /** - * Define the normalizr schema for the relationship. - */ - define(schema: Schema): NormalizrSchema { - return schema.many(this.related) - } - - /** - * Attach the relational key to the given data. Since morphed by many - * relationship doesn't have any foreign key, it would do nothing. - */ - attach(_key: any, _record: Record, _data: NormalizedData): void { - return - } - - /** - * Make value to be set to model property. This method is used when - * instantiating a model or creating a plain object from a model. - */ - make(value: any, _parent: Record, _key: string): Model[] { - return this.makeManyRelation(value, this.related) - } - - /** - * Load the morph many relationship for the record. - */ - load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void { - const relatedQuery = this.getRelation( - query, - this.related.entity, - constraints - ) - - const pivotQuery = query.newQuery(this.pivot.entity) - - this.addEagerConstraintForPivot(pivotQuery, collection, this.related.entity) - - const pivots = pivotQuery.get() - - this.addEagerConstraintForRelated(relatedQuery, pivots) - - const relateds = this.mapPivotRelations(pivots, relatedQuery) - - collection.forEach((item) => { - const related = relateds[item[this.parentKey]] - - item[name] = related || [] - }) - } - - /** - * Set the constraints for the pivot relation. - */ - addEagerConstraintForPivot( - query: Query, - collection: Collection, - type: string - ): void { - query - .whereFk(this.type, type) - .whereFk(this.relatedId, this.getKeys(collection, this.parentKey)) - } - - /** - * Set the constraints for the related relation. - */ - addEagerConstraintForRelated(query: Query, collection: Collection): void { - query.whereFk(this.relatedKey, this.getKeys(collection, this.id)) - } - - /** - * Create a new indexed map for the pivot relation. - */ - mapPivotRelations(pivots: Collection, relatedQuery: Query): Records { - const relations = this.mapManyRelations(relatedQuery.get(), this.relatedKey) - - if (relatedQuery.orders.length) { - return this.mapRelationsByOrders( - pivots, - relations, - this.relatedId, - this.id - ) - } - - return pivots.reduce((records, record) => { - const id = record[this.relatedId] - - if (!records[id]) { - records[id] = [] - } - - const related = relations.get(record[this.id]) - - /* istanbul ignore if */ - if (related === undefined || related.length === 0) { - return records - } - - records[id] = records[id].concat( - related.map((model: Record) => { - model[this.pivotKey] = record - return model - }) - ) - - return records - }, {}) - } - - /** - * Create pivot records for the given records if needed. - */ - createPivots( - parent: typeof Model, - data: NormalizedData, - key: string - ): NormalizedData { - Utils.forOwn(data[parent.entity], (record) => { - const related = record[key] - - if (!Utils.isArray(related)) { - return - } - - this.createPivotRecord(data, record, related) - }) - - return data - } - - /** - * Create a pivot record. - */ - createPivotRecord( - data: NormalizedData, - record: Record, - related: any[] - ): void { - related.forEach((id) => { - const parentId = record[this.parentKey] - const pivotKey = `${id}_${parentId}_${this.related.entity}` - const pivotData = data[this.related.entity][id][this.pivotKey] || {} - - data[this.pivot.entity] = { - ...data[this.pivot.entity], - - [pivotKey]: { - ...pivotData, - $id: pivotKey, - [this.relatedId]: parentId, - [this.id]: this.model.getIdFromRecord(data[this.related.entity][id]), - [this.type]: this.related.entity - } - } - }) - } -} diff --git a/src/attributes/relations/Relation.ts b/src/attributes/relations/Relation.ts deleted file mode 100644 index 6a45b81b..00000000 --- a/src/attributes/relations/Relation.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { Schema as NormalizrSchema } from 'normalizr' -import { isArray } from '../../support/Utils' -import Schema from '../../schema/Schema' -import { Record, Records, NormalizedData, Collection } from '../../data' -import Model from '../../model/Model' -import Query from '../../query/Query' -import Constraint from '../../query/contracts/RelationshipConstraint' -import Attribute from '../Attribute' - -export default abstract class Relation extends Attribute { - /** - * Define the normalizr schema for the relationship. - */ - abstract define(schema: Schema): NormalizrSchema - - /** - * Attach the relational key to the given data. Basically, this method - * should attach any missing foreign keys to the normalized data. - */ - abstract attach(key: any, record: Record, data: NormalizedData): void - - /** - * Load relationship records. - */ - abstract load( - query: Query, - collection: Collection, - name: string, - constraints: Constraint[] - ): void - - /** - * Convert given value to the appropriate value for the attribute. - */ - abstract make(value: any, parent: Record, key: string): Model | Model[] | null - - /** - * Get relation query instance with constraint attached. - */ - protected getRelation( - query: Query, - name: string, - constraints: Constraint[] - ): Query { - const relation = query.newQuery(name) - - constraints.forEach((constraint) => { - constraint(relation) - }) - - return relation - } - - /** - * Get specified keys from the given collection. - */ - protected getKeys(collection: Collection, key: string): string[] { - return collection.reduce((models, model) => { - if (model[key] === null || model[key] === undefined) { - return models - } - - models.push(model[key] as string) - - return models - }, []) - } - - /** - * Create a new indexed map for the single relation by specified key. - */ - mapSingleRelations(collection: Record[], key: string): Map { - const relations = new Map() - - collection.forEach((record) => { - const id = record[key] - - !relations.get(id) && relations.set(id, record) - }) - - return relations - } - - /** - * Create a new indexed map for the many relation by specified key. - */ - mapManyRelations(collection: Record[], key: string): Map { - const relations = new Map() - - collection.forEach((record) => { - const id = record[key] - - let ownerKeys = relations.get(id) - - if (!ownerKeys) { - ownerKeys = [] - relations.set(id, ownerKeys) - } - - ownerKeys.push(record) - }) - - return relations - } - - /** - * Create a new indexed map for relations with order constraints. - */ - mapRelationsByOrders( - collection: Collection, - relations: Map, - ownerKey: string, - relationKey: string - ): Records { - const records: Records = {} - - relations.forEach((related, id) => { - collection - .filter((record) => record[relationKey] === id) - .forEach((record) => { - const id = record[ownerKey] - - if (!records[id]) { - records[id] = [] - } - - records[id] = records[id].concat(related) - }) - }) - - return records - } - - /** - * Check if the given record is a single relation, which is an object. - */ - isOneRelation(record: any): boolean { - if (!isArray(record) && record !== null && typeof record === 'object') { - return true - } - - return false - } - - /** - * Check if the given records is a many relation, which is an array - * of object. - */ - isManyRelation(records: any): boolean { - if (!isArray(records)) { - return false - } - - if (records.length < 1) { - return false - } - - return true - } - - /** - * Wrap the given object into a model instance. - */ - makeOneRelation(record: any, model: typeof Model): Model | null { - if (!this.isOneRelation(record)) { - return null - } - - const relatedModel = model.getModelFromRecord(record) || model - - return new relatedModel(record) - } - - /** - * Wrap the given records into a collection of model instances. - */ - makeManyRelation(records: any, model: typeof Model): Collection { - if (!this.isManyRelation(records)) { - return [] - } - - return records - .filter((record: any) => { - return this.isOneRelation(record) - }) - .map((record: Record) => { - const relatedModel = model.getModelFromRecord(record) || model - - return new relatedModel(record) - }) - } -} diff --git a/src/attributes/types/Attr.ts b/src/attributes/types/Attr.ts deleted file mode 100644 index e6363b5c..00000000 --- a/src/attributes/types/Attr.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Record from '../../data/Record' -import Model from '../../model/Model' -import Mutator from '../contracts/Mutator' -import Type from './Type' - -export default class Attr extends Type { - /** - * Create a new attr instance. - */ - constructor(model: typeof Model, value: any, mutator?: Mutator) { - /* istanbul ignore next */ - super(model, value, mutator) - } - - /** - * Make value to be set to model property. This method is used when - * instantiating a model or creating a plain object from a model. - */ - make(value: any, _parent: Record, key: string): any { - value = value !== undefined ? value : this.value - - // Default Value might be a function (taking no parameter). - let localValue = value - - if (typeof value === 'function') { - localValue = value() - } - - return this.mutate(localValue, key) - } -} diff --git a/src/attributes/types/Boolean.ts b/src/attributes/types/Boolean.ts deleted file mode 100644 index a80132f8..00000000 --- a/src/attributes/types/Boolean.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Record from '../../data/Record' -import Model from '../../model/Model' -import Mutator from '../contracts/Mutator' -import Type from './Type' - -export default class Boolean extends Type { - /** - * Create a new number instance. - */ - constructor( - model: typeof Model, - value: boolean, - mutator?: Mutator - ) { - /* istanbul ignore next */ - super(model, value, mutator) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, key: string): boolean | null { - return this.mutate(this.fix(value), key) - } - - /** - * Transform given data to the boolean. - */ - fix(value: any): boolean | null { - if (value === undefined) { - return this.value - } - - if (typeof value === 'boolean') { - return value - } - - if (typeof value === 'string') { - if (value.length === 0) { - return false - } - - const int = parseInt(value, 0) - - return isNaN(int) ? true : !!int - } - - if (typeof value === 'number') { - return !!value - } - - if (value === null && this.isNullable) { - return value - } - - return false - } -} diff --git a/src/attributes/types/Number.ts b/src/attributes/types/Number.ts deleted file mode 100644 index 0509416b..00000000 --- a/src/attributes/types/Number.ts +++ /dev/null @@ -1,52 +0,0 @@ -import Record from '../../data/Record' -import Model from '../../model/Model' -import Mutator from '../contracts/Mutator' -import Type from './Type' - -export default class Number extends Type { - /** - * Create a new number instance. - */ - constructor( - model: typeof Model, - value: number | null, - mutator?: Mutator - ) { - /* istanbul ignore next */ - super(model, value, mutator) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, key: string): number | null { - return this.mutate(this.fix(value), key) - } - - /** - * Transform given data to the number. - */ - fix(value: any): number | null { - if (value === undefined) { - return this.value - } - - if (typeof value === 'number') { - return value - } - - if (typeof value === 'string') { - return parseFloat(value) - } - - if (typeof value === 'boolean') { - return value ? 1 : 0 - } - - if (value === null && this.isNullable) { - return value - } - - return 0 - } -} diff --git a/src/attributes/types/String.ts b/src/attributes/types/String.ts deleted file mode 100644 index 1ba6202a..00000000 --- a/src/attributes/types/String.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Record from '../../data/Record' -import Model from '../../model/Model' -import Mutator from '../contracts/Mutator' -import Type from './Type' - -export default class String extends Type { - /** - * Create a new string instance. - */ - constructor( - model: typeof Model, - value: string | null, - mutator?: Mutator - ) { - /* istanbul ignore next */ - super(model, value, mutator) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value: any, _parent: Record, key: string): string | null { - return this.mutate(this.fix(value), key) - } - - /** - * Convert given value to the string. - */ - fix(value: any): string | null { - if (value === undefined) { - return this.value - } - - if (typeof value === 'string') { - return value - } - - if (value === null && this.isNullable) { - return value - } - - return value + '' - } -} diff --git a/src/attributes/types/Type.ts b/src/attributes/types/Type.ts deleted file mode 100644 index f8ea01bb..00000000 --- a/src/attributes/types/Type.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Model from '../../model/Model' -import Mutator from '../contracts/Mutator' -import Attribute from '../Attribute' - -export default abstract class Type extends Attribute { - /** - * Whether if the attribute can accept `null` as a value. - */ - isNullable: boolean = false - - /** - * The mutator for the field. - */ - mutator?: Mutator - - /** - * Create a new type instance. - */ - constructor(model: typeof Model, value: any, mutator?: (value: any) => any) { - super(model) /* istanbul ignore next */ - - this.value = value - this.mutator = mutator - } - - /** - * Set `isNullable` to be `true`. - */ - nullable(): this { - this.isNullable = true - - return this - } - - /** - * Mutate the given value by mutator. - */ - mutate(value: any, key: string): any { - const mutator = this.mutator || this.model.mutators()[key] - - return mutator ? mutator(value) : value - } -} diff --git a/src/attributes/types/Uid.ts b/src/attributes/types/Uid.ts deleted file mode 100644 index 42ec962c..00000000 --- a/src/attributes/types/Uid.ts +++ /dev/null @@ -1,28 +0,0 @@ -import UidGenerator from '../../support/Uid' -import Model from '../../model/Model' -import Type from './Type' - -export default class Uid extends Type { - /** - * Create a new uid instance. - */ - constructor(model: typeof Model, value?: () => string | number) { - /* istanbul ignore next */ - super(model, value) - } - - /** - * Convert given value to the appropriate value for the attribute. - */ - make(value?: any): string | number | null { - if (typeof value === 'number' || typeof value === 'string') { - return value - } - - if (typeof this.value === 'function') { - return this.value() - } - - return UidGenerator.make() - } -} diff --git a/src/connections/Connection.ts b/src/connections/Connection.ts deleted file mode 100644 index 75ddcb8c..00000000 --- a/src/connections/Connection.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Store } from 'vuex' -import { Records, RecordWithId } from '../data' -import RootState from '../modules/contracts/RootState' -import State from '../modules/contracts/State' - -export default class Connection { - /** - * The store instance. - */ - store: Store - - /** - * The connection name. - */ - connection: string - - /** - * The entity name. - */ - entity: string - - /** - * The root state. - */ - rootState: RootState - - /** - * The entity state. - */ - state: State - - /** - * Create a new connection instance. - */ - constructor(store: Store, connection: string, entity: string) { - this.store = store - this.connection = connection - this.entity = entity - this.rootState = this.store.state[connection] - this.state = this.rootState[entity] - } - - /** - * Commit Vuex Mutation. - */ - private commit(name: string, payload: any): void { - this.store.commit(`${this.connection}/${name}`, payload) - } - - /** - * Commit the given record by `new` method. - */ - new(record: RecordWithId): void { - this.commit('new', { - entity: this.entity, - data: { [record.$id]: record } - }) - } - - /** - * Insert the given record. - */ - insert(records: RecordWithId[]): void { - this.commit('new', { - entity: this.entity, - data: this.mapRecordToRecords(records) - }) - } - - /** - * Insert the given records. - */ - insertRecords(records: Records): void { - this.state.data = { ...this.state.data, ...records } - } - - /** - * Delete records that matches the given id. - */ - delete(id: string[]): void { - const data: Records = {} - - for (const i in this.state.data) { - if (!id.includes(i)) { - data[i] = this.state.data[i] - } - } - - this.state.data = data - } - - /** - * Map the given records to `Records` type object. - */ - private mapRecordToRecords(records: RecordWithId[]): Records { - return records.reduce((carry, record) => { - carry[record.$id] = record - return carry - }, {}) - } -} diff --git a/src/container/Container.ts b/src/container/Container.ts deleted file mode 100644 index 9b2d170e..00000000 --- a/src/container/Container.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Store } from 'vuex' - -export default class Container { - /** - * The store instance. - */ - static store: Store - - /** - * Register the store instance. - */ - static register(store: Store): void { - this.store = store - } -} diff --git a/src/data/Collection.ts b/src/data/Collection.ts deleted file mode 100644 index 3f04dbff..00000000 --- a/src/data/Collection.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Model from '../model/Model' -import Instance from './Instance' - -export type Collection = Instance[] - -export default Collection diff --git a/src/data/Collections.ts b/src/data/Collections.ts deleted file mode 100644 index 348392b8..00000000 --- a/src/data/Collections.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Collection from './Collection' - -export interface Collections { - [entity: string]: Collection -} - -export default Collections diff --git a/src/data/Entities.ts b/src/data/Entities.ts deleted file mode 100644 index 1e9f3846..00000000 --- a/src/data/Entities.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Instances from './Instances' - -export interface Entities { - [entity: string]: Instances -} - -export default Entities diff --git a/src/data/Instance.ts b/src/data/Instance.ts deleted file mode 100644 index 1f09eae6..00000000 --- a/src/data/Instance.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Model from '../model/Model' - -export type Instance = M - -export default Instance diff --git a/src/data/InstanceOf.ts b/src/data/InstanceOf.ts deleted file mode 100644 index 62e651d3..00000000 --- a/src/data/InstanceOf.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type InstanceOf = T extends new (...args: any[]) => infer R ? R : any - -export default InstanceOf diff --git a/src/data/Instances.ts b/src/data/Instances.ts deleted file mode 100644 index 2ca12741..00000000 --- a/src/data/Instances.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Instance from './Instance' -import Model from '../model/Model' - -export interface Instances { - [id: string]: Instance -} - -export default Instances diff --git a/src/data/Item.ts b/src/data/Item.ts deleted file mode 100644 index 2494c73f..00000000 --- a/src/data/Item.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Model from '../model/Model' -import Instance from './Instance' - -export type Item = Instance | null - -export default Item diff --git a/src/data/NormalizedData.ts b/src/data/NormalizedData.ts deleted file mode 100644 index 00867086..00000000 --- a/src/data/NormalizedData.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Records from './Records' - -export interface NormalizedData { - [entity: string]: Records -} - -export default NormalizedData diff --git a/src/data/Record.ts b/src/data/Record.ts deleted file mode 100644 index ae68fd37..00000000 --- a/src/data/Record.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Record { - [field: string]: any -} - -export default Record diff --git a/src/data/RecordWithId.ts b/src/data/RecordWithId.ts deleted file mode 100644 index 60c7b94c..00000000 --- a/src/data/RecordWithId.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Record from './Record' - -export interface RecordWithId extends Record { - $id: string -} - -export default RecordWithId diff --git a/src/data/Records.ts b/src/data/Records.ts deleted file mode 100644 index b70cb1af..00000000 --- a/src/data/Records.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Record from './Record' - -export interface Records { - [id: string]: Record -} - -export default Records diff --git a/src/data/index.ts b/src/data/index.ts deleted file mode 100644 index 19fb4667..00000000 --- a/src/data/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Record from './Record' -import RecordWithId from './RecordWithId' -import Records from './Records' -import NormalizedData from './NormalizedData' -import Instance from './Instance' -import Instances from './Instances' -import InstanceOf from './InstanceOf' -import Entities from './Entities' -import Item from './Item' -import Collection from './Collection' -import Collections from './Collections' - -export { - Record, - RecordWithId, - Records, - NormalizedData, - InstanceOf, - Instance, - Instances, - Entities, - Item, - Collection, - Collections -} diff --git a/src/database/Database.ts b/src/database/Database.ts deleted file mode 100644 index 4bb6c97a..00000000 --- a/src/database/Database.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { Store, Module, MutationTree } from 'vuex' -import { mapValues } from '../support/Utils' -import Schema from '../schema/Schema' -import Schemas from '../schema/Schemas' -import Model from '../model/Model' -import ModuleContract from '../modules/contracts/Module' -import RootState from '../modules/contracts/RootState' -import RootGettersContract from '../modules/contracts/RootGetters' -import RootActionsContract from '../modules/contracts/RootActions' -import RootMutationsContract from '../modules/contracts/RootMutations' -import RootGetters from '../modules/RootGetters' -import RootActions from '../modules/RootActions' -import RootMutations from '../modules/RootMutations' -import State from '../modules/contracts/State' -import GettersContract from '../modules/contracts/Getters' -import ActionsContract from '../modules/contracts/Actions' -import Getters from '../modules/Getters' -import Actions from '../modules/Actions' - -export interface Entity { - name: string - base: string - model: typeof Model - module: Module -} - -export type Models = Record -export type Modules = Record> - -export default class Database { - /** - * The Vuex Store instance. - */ - store!: Store - - /** - * The namespace for the Vuex Module. Vuex ORM will create Vuex Modules from the - * registered models and modules and define them under this namespace. - */ - namespace!: string - - /** - * The list of entities. It contains models and modules with its name. - * The name is going to be the namespace for the Vuex Modules. - */ - entities: Entity[] = [] - - /** - * The hacked models registered to the store. - */ - hackedModels: Models = {} - - /** - * The normalizr schema. - */ - schemas: Schemas = {} - - /** - * Whether the database has already been installed to Vuex or not. - * Model registration steps depend on its value. - */ - isStarted: boolean = false - - /** - * Initialize the database before a user can start using it. - */ - start(store: Store, namespace: string): void { - this.store = store - this.namespace = namespace - - this.registerModules() - this.createSchema() - - this.isStarted = true - } - - /** - * Register a model and a module to Database. - */ - register(model: typeof Model, module: Module = {}): void { - this.checkModelTypeMappingCapability(model) - - const entity: Entity = { - name: model.entity, - base: model.baseEntity || model.entity, - model, - module - } - - this.hackedModels[model.entity] = this.createBindingModel(model) - - this.entities.push(entity) - - if (this.isStarted) { - this.registerModule(entity) - this.registerSchema(entity) - } - } - - /** - * Get the model of the given name from the entities list. - */ - model(model: T): T - model(model: string): typeof Model - model(model: typeof Model | string): typeof Model | string { - const name = typeof model === 'string' ? model : model.entity - const m = this.models()[name] - - if (!m) { - throw new Error( - `[Vuex ORM] Could not find the model \`${name}\`. Please check if you ` + - 'have registered the model to the database.' - ) - } - - return m - } - - /** - * Get the base model of the given name from the entities list. - */ - baseModel(model: T): T - baseModel(model: string): typeof Model - baseModel(model: typeof Model | string): typeof Model | string { - const name = typeof model === 'string' ? model : model.entity - const m = this.baseModels()[name] - - if (!m) { - throw new Error( - `[Vuex ORM] Could not find the model \`${name}\`. Please check if you ` + - 'have registered the model to the database.' - ) - } - - return m - } - - /** - * Get all models from the entities list. - */ - models(): Models { - return this.entities.reduce((models, entity) => { - models[entity.name] = entity.model - return models - }, {}) - } - - /** - * Get all base models from the entities list. - */ - baseModels(): Models { - return this.entities.reduce((models, entity) => { - models[entity.name] = this.model(entity.base) - return models - }, {}) - } - - /** - * Get the module of the given name from the entities list. - */ - module(name: string): Module { - const module = this.modules()[name] - - if (!module) { - throw new Error( - `[Vuex ORM] Could not find the module \`${name}\`. Please check if you ` + - 'have registered the module to the database.' - ) - } - - return module - } - - /** - * Get all modules from the entities list. - */ - modules(): Modules { - return this.entities.reduce((modules, entity) => { - modules[entity.name] = entity.module - return modules - }, {}) - } - - /** - * Get the root state from the store. - */ - getState(): RootState { - return this.store.state[this.namespace] - } - - /** - * Create a new model that binds the database. - * - * Transpiled classes cannot extend native classes. Implemented a workaround - * until Babel releases a fix (https://github.com/babel/babel/issues/9367). - */ - private createBindingModel(model: typeof Model): typeof Model { - let proxy: typeof Model - - try { - proxy = new Function( - 'model', - ` - 'use strict'; - return class ${model.name} extends model {} - ` - )(model) - } catch { - /* istanbul ignore next: rollback (mostly <= IE10) */ - proxy = class extends model {} - - /* istanbul ignore next: allocate model name */ - Object.defineProperty(proxy, 'name', { get: () => model.name }) - } - - Object.defineProperty(proxy, 'store', { - value: (): Store => this.store - }) - - return proxy - } - - /** - * Create Vuex Module from the registered entities, and register to - * the store. - */ - private registerModules(): void { - this.store.registerModule(this.namespace, this.createModule()) - } - - /** - * Generate module from the given entity, and register to the store. - */ - private registerModule(entity: Entity): void { - this.store.registerModule( - [this.namespace, entity.name], - this.createSubModule(entity) - ) - } - - /** - * Create Vuex Module from the registered entities. - */ - private createModule(): Module { - const module = this.createRootModule() - - this.entities.forEach((entity) => { - module.modules[entity.name] = this.createSubModule(entity) - }) - - return module - } - - /** - * Create root module. - */ - private createRootModule(): ModuleContract { - return { - namespaced: true, - state: this.createRootState(), - getters: this.createRootGetters(), - actions: this.createRootActions(), - mutations: this.createRootMutations(), - modules: {} - } - } - - /** - * Create root state. - */ - private createRootState(): () => RootState { - return () => ({ $name: this.namespace }) - } - - /** - * Create root getters. For the getters, we bind the store instance to each - * function to retrieve database instances within getters. We only need this - * for the getter since actions and mutations are already bound to store. - */ - private createRootGetters(): RootGettersContract { - return mapValues(RootGetters, (_getter, name) => { - return RootGetters[name].bind(this.store) - }) - } - - /** - * Create root actions. - */ - private createRootActions(): RootActionsContract { - return RootActions - } - - /** - * Create root mutations. - */ - private createRootMutations(): RootMutationsContract { - return RootMutations - } - - /** - * Create sub module. - */ - private createSubModule(entity: Entity): Module { - return { - namespaced: true, - state: this.createSubState(entity), - getters: this.createSubGetters(entity), - actions: this.createSubActions(entity), - mutations: this.createSubMutations(entity) - } - } - - /** - * Create sub state. - */ - private createSubState(entity: Entity): () => State { - const { name, model, module } = entity - - const modelState = - typeof model.state === 'function' ? model.state() : model.state - const moduleState = - typeof module.state === 'function' ? module.state() : module.state - - return () => ({ - ...modelState, - ...moduleState, - $connection: this.namespace, - $name: name, - data: {} - }) - } - - /** - * Create sub getters. - */ - private createSubGetters(entity: Entity): GettersContract { - return { ...Getters, ...entity.module.getters } - } - - /** - * Create sub actions. - */ - private createSubActions(entity: Entity): ActionsContract { - return { ...Actions, ...entity.module.actions } - } - - /** - * Create sub mutations. - */ - private createSubMutations(entity: Entity): MutationTree { - return entity.module.mutations ?? {} - } - - /** - * Create the schema definition from registered entities list and set it to - * the `schema` property. This schema will be used by the normalizer - * to normalize data before persisting them to the Vuex Store. - */ - private createSchema(): void { - this.entities.forEach((entity) => { - this.registerSchema(entity) - }) - } - - /** - * Generate schema from the given entity. - */ - private registerSchema(entity: Entity): void { - this.schemas[entity.name] = Schema.create(entity.model) - } - - /** - * Warn user if the given model is a type of an inherited model that is being - * defined without overwriting `Model.types()` because the user will not be - * able to use the type mapping feature in this case. - */ - private checkModelTypeMappingCapability(model: typeof Model): void { - // We'll not be logging any warning if it's on a production environment, - // so let's return here if it is. - /* istanbul ignore next */ - if (!__DEV__) { - return - } - - // If the model doesn't have `baseEntity` property set, we'll assume it is - // not an inherited model so we can stop here. - if (!model.baseEntity) { - return - } - - // Now it seems like the model is indeed an inherited model. Let's check if - // it has `types()` method declared, or we'll warn the user that it's not - // possible to use type mapping feature. - const baseModel = this.model(model.baseEntity) - - if (baseModel && baseModel.types === Model.types) { - console.warn( - `[Vuex ORM] Model \`${model.name}\` extends \`${baseModel.name}\` which doesn't ` + - 'overwrite Model.types(). You will not be able to use type mapping.' - ) - } - } -} diff --git a/src/database/HackedDatabase.ts b/src/database/HackedDatabase.ts deleted file mode 100644 index 71b1302c..00000000 --- a/src/database/HackedDatabase.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Database from './Database' -import Model from '../model/Model' - -export default class HackedDatabase { - /** - * The database instance to look up models. - */ - database: Database - - /** - * Create a new hacked database. - */ - constructor(database: Database) { - this.database = database - } - - /** - * Get the hacked model of the given name from the entities list. - */ - model(model: M): M - model(model: string): typeof Model - model(model: typeof Model | string): typeof Model | string { - /* istanbul ignore next */ - const name = typeof model === 'string' ? model : model.entity - - return this.database.hackedModels[name] - } -} diff --git a/src/index.cjs.ts b/src/index.cjs.ts index c0159d6c..c15f26f4 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,69 +1 @@ -import './polyfills' - -import { install } from './store' -import { use } from './plugins/use' -import Container from './container/Container' -import Database from './database/Database' -import Model from './model/Model' -import Query from './query/Query' -import Attribute from './attributes/Attribute' -import Type from './attributes/types/Type' -import Attr from './attributes/types/Attr' -import String from './attributes/types/String' -import Number from './attributes/types/Number' -import Boolean from './attributes/types/Boolean' -import Uid from './attributes/types/Uid' -import Relation from './attributes/relations/Relation' -import HasOne from './attributes/relations/HasOne' -import BelongsTo from './attributes/relations/BelongsTo' -import HasMany from './attributes/relations/HasMany' -import HasManyBy from './attributes/relations/HasManyBy' -import BelongsToMany from './attributes/relations/BelongsToMany' -import HasOneThrough from './attributes/relations/HasOneThrough' -import HasManyThrough from './attributes/relations/HasManyThrough' -import MorphTo from './attributes/relations/MorphTo' -import MorphOne from './attributes/relations/MorphOne' -import MorphMany from './attributes/relations/MorphMany' -import MorphToMany from './attributes/relations/MorphToMany' -import MorphedByMany from './attributes/relations/MorphedByMany' -import Repository from './repository/Repository' -import Getters from './modules/Getters' -import Actions from './modules/Actions' -import RootGetters from './modules/RootGetters' -import RootActions from './modules/RootActions' -import RootMutations from './modules/RootMutations' - -export default { - install, - use, - Container, - Database, - Model, - Attribute, - Type, - Attr, - String, - Number, - Boolean, - Uid, - Relation, - HasOne, - BelongsTo, - HasMany, - HasManyBy, - BelongsToMany, - HasOneThrough, - HasManyThrough, - MorphTo, - MorphOne, - MorphMany, - MorphToMany, - MorphedByMany, - Repository, - Getters, - Actions, - RootGetters, - RootActions, - RootMutations, - Query -} +// ... diff --git a/src/index.ts b/src/index.ts index bcc101f0..c15f26f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,128 +1 @@ -import './types/vuex' -import './polyfills' - -import { install } from './store' -import { use, PluginComponents } from './plugins/use' -import Container from './container/Container' -import Database from './database/Database' -import Model from './model/Model' -import Fields from './model/contracts/Fields' -import Attribute from './attributes/Attribute' -import Type from './attributes/types/Type' -import Attr from './attributes/types/Attr' -import String from './attributes/types/String' -import Number from './attributes/types/Number' -import Boolean from './attributes/types/Boolean' -import Uid from './attributes/types/Uid' -import Relation from './attributes/relations/Relation' -import HasOne from './attributes/relations/HasOne' -import BelongsTo from './attributes/relations/BelongsTo' -import HasMany from './attributes/relations/HasMany' -import HasManyBy from './attributes/relations/HasManyBy' -import BelongsToMany from './attributes/relations/BelongsToMany' -import HasOneThrough from './attributes/relations/HasOneThrough' -import HasManyThrough from './attributes/relations/HasManyThrough' -import MorphTo from './attributes/relations/MorphTo' -import MorphOne from './attributes/relations/MorphOne' -import MorphMany from './attributes/relations/MorphMany' -import MorphToMany from './attributes/relations/MorphToMany' -import MorphedByMany from './attributes/relations/MorphedByMany' -import Repository from './repository/Repository' -import Getters from './modules/Getters' -import Actions from './modules/Actions' -import RootGetters from './modules/RootGetters' -import RootActions from './modules/RootActions' -import RootMutations from './modules/RootMutations' -import Query from './query/Query' -import Record from './data/Record' -import Records from './data/Records' -import NormalizedData from './data/NormalizedData' -import Instance from './data/Instance' -import Instances from './data/Instances' -import InstanceOf from './data/InstanceOf' -import Entities from './data/Entities' -import Item from './data/Item' -import Collection from './data/Collection' -import Collections from './data/Collections' - -export { - install, - use, - PluginComponents, - Container, - Database, - Model, - Fields, - Attribute, - Type, - Attr, - String, - Number, - Boolean, - Uid, - Relation, - HasOne, - BelongsTo, - HasMany, - HasManyBy, - BelongsToMany, - HasOneThrough, - HasManyThrough, - MorphTo, - MorphOne, - MorphMany, - MorphToMany, - MorphedByMany, - Repository, - Getters, - Actions, - RootGetters, - RootActions, - RootMutations, - Query, - Record, - Records, - NormalizedData, - Instance, - Instances, - InstanceOf, - Entities, - Item, - Collection, - Collections -} - -export default { - install, - use, - Container, - Database, - Model, - Attribute, - Type, - Attr, - String, - Number, - Boolean, - Uid, - Relation, - HasOne, - BelongsTo, - HasMany, - HasManyBy, - BelongsToMany, - HasOneThrough, - HasManyThrough, - MorphTo, - MorphOne, - MorphMany, - MorphToMany, - MorphedByMany, - Repository, - Getters, - Actions, - RootGetters, - RootActions, - RootMutations, - Query -} +// ... diff --git a/src/model/Model.ts b/src/model/Model.ts deleted file mode 100644 index 43f83b2f..00000000 --- a/src/model/Model.ts +++ /dev/null @@ -1,1076 +0,0 @@ -import * as Vuex from 'vuex' -import Utils from '../support/Utils' -import Uid from '../support/Uid' -import Container from '../container/Container' -import Database from '../database/Database' -import Record from '../data/Record' -import RecordWithId from '../data/RecordWithId' -import InstanceOf from '../data/InstanceOf' -import Item from '../data/Item' -import Collection from '../data/Collection' -import Collections from '../data/Collections' -import State from '../modules/contracts/State' -import * as Attributes from '../attributes' -import Mutator from '../attributes/contracts/Mutator' -import Mutators from '../attributes/contracts/Mutators' -import Predicate from '../query/contracts/Predicate' -import Query from '../query/Query' -import * as Payloads from '../modules/payloads/Actions' -import * as PersistOptions from '../modules/options/Actions' -import Fields from './contracts/Fields' -import FieldCache from './contracts/FieldCache' -import ModelState from './contracts/State' -import InheritanceTypes from './contracts/InheritanceTypes' -import { toAttributes, toJson } from './Serialize' -import PayloadBuilder from '../modules/support/PayloadBuilder' -import { PersistOptions as QueryPersistOptions } from '../query/options' -import { InsertObject, normalizeInsertPayload } from './support/Payloads' - -export default class Model { - /** - * The name that is going be used as module name in Vuex Store. - */ - static entity: string - - /** - * The reference to the base entity name if the class extends a base entity. - */ - static baseEntity: string - - /** - * The primary key to be used for the model. - */ - static primaryKey: string | string[] = 'id' - - /** - * The discriminator key to be used for the model when inheritance is used - */ - static typeKey: string = 'type' - - /** - * Vuex Store state definition. - */ - static state: ModelState | (() => ModelState) = {} - - /** - * The cached attribute fields of the model. - */ - static cachedFields: FieldCache - - /** - * The index ID for the model. - */ - $id: string | null = null - - /** - * Create a new model instance. - */ - constructor(record?: Record) { - this.$fill(record) - } - - /** - * The definition of the fields of the model and its relations. - */ - static fields(): Fields { - return {} - } - - /** - * Create an attr attribute. - */ - static attr(value: any, mutator?: Mutator): Attributes.Attr { - return new Attributes.Attr(this, value, mutator) - } - - /** - * Create a string attribute. - */ - static string( - value: any, - mutator?: Mutator - ): Attributes.String { - return new Attributes.String(this, value, mutator) - } - - /** - * Create a number attribute. - */ - static number( - value: any, - mutator?: Mutator - ): Attributes.Number { - return new Attributes.Number(this, value, mutator) - } - - /** - * Create a boolean attribute. - */ - static boolean( - value: any, - mutator?: Mutator - ): Attributes.Boolean { - return new Attributes.Boolean(this, value, mutator) - } - - /** - * Create an uid attribute. - */ - static uid(value?: () => string | number): Attributes.Uid { - return new Attributes.Uid(this, value) - } - - /** - * @deprecated Use `uid` attribute instead. - */ - static increment(): Attributes.Uid { - /* istanbul ignore next */ - if (__DEV__) { - console.warn( - '[Vuex ORM] Attribute type `increment` has been deprecated and replaced with `uid`.' - ) - } - - return this.uid() - } - - /** - * Create a has one relationship. - */ - static hasOne( - related: typeof Model | string, - foreignKey: string, - localKey?: string - ): Attributes.HasOne { - return new Attributes.HasOne( - this, - related, - foreignKey, - this.localKey(localKey) - ) - } - - /** - * Create a belongs to relationship. - */ - static belongsTo( - parent: typeof Model | string, - foreignKey: string, - ownerKey?: string - ): Attributes.BelongsTo { - return new Attributes.BelongsTo( - this, - parent, - foreignKey, - this.relation(parent).localKey(ownerKey) - ) - } - - /** - * Create a has many relationship. - */ - static hasMany( - related: typeof Model | string, - foreignKey: string, - localKey?: string - ): Attributes.HasMany { - return new Attributes.HasMany( - this, - related, - foreignKey, - this.localKey(localKey) - ) - } - - /** - * Create a has many by relationship. - */ - static hasManyBy( - parent: typeof Model | string, - foreignKey: string, - ownerKey?: string - ): Attributes.HasManyBy { - return new Attributes.HasManyBy( - this, - parent, - foreignKey, - this.relation(parent).localKey(ownerKey) - ) - } - - /** - * Create a has one through relationship. - */ - static hasOneThrough( - related: typeof Model | string, - through: typeof Model | string, - firstKey: string, - secondKey: string, - localKey?: string, - secondLocalKey?: string - ): Attributes.HasOneThrough { - return new Attributes.HasOneThrough( - this, - related, - through, - firstKey, - secondKey, - this.localKey(localKey), - this.relation(through).localKey(secondLocalKey) - ) - } - - /** - * Create a has many through relationship. - */ - static hasManyThrough( - related: typeof Model | string, - through: typeof Model | string, - firstKey: string, - secondKey: string, - localKey?: string, - secondLocalKey?: string - ): Attributes.HasManyThrough { - return new Attributes.HasManyThrough( - this, - related, - through, - firstKey, - secondKey, - this.localKey(localKey), - this.relation(through).localKey(secondLocalKey) - ) - } - - /** - * Create a belongs to many relationship. - */ - static belongsToMany( - related: typeof Model | string, - pivot: typeof Model | string, - foreignPivotKey: string, - relatedPivotKey: string, - parentKey?: string, - relatedKey?: string - ): Attributes.BelongsToMany { - return new Attributes.BelongsToMany( - this, - related, - pivot, - foreignPivotKey, - relatedPivotKey, - this.localKey(parentKey), - this.relation(related).localKey(relatedKey) - ) - } - - /** - * Create a morph to relationship. - */ - static morphTo(id: string, type: string): Attributes.MorphTo { - return new Attributes.MorphTo(this, id, type) - } - - /** - * Create a morph one relationship. - */ - static morphOne( - related: typeof Model | string, - id: string, - type: string, - localKey?: string - ): Attributes.MorphOne { - return new Attributes.MorphOne( - this, - related, - id, - type, - this.localKey(localKey) - ) - } - - /** - * Create a morph many relationship. - */ - static morphMany( - related: typeof Model | string, - id: string, - type: string, - localKey?: string - ): Attributes.MorphMany { - return new Attributes.MorphMany( - this, - related, - id, - type, - this.localKey(localKey) - ) - } - - /** - * Create a morph to many relationship. - */ - static morphToMany( - related: typeof Model | string, - pivot: typeof Model | string, - relatedId: string, - id: string, - type: string, - parentKey?: string, - relatedKey?: string - ): Attributes.MorphToMany { - return new Attributes.MorphToMany( - this, - related, - pivot, - relatedId, - id, - type, - this.localKey(parentKey), - this.relation(related).localKey(relatedKey) - ) - } - - /** - * Create a morphed by many relationship. - */ - static morphedByMany( - related: typeof Model | string, - pivot: typeof Model | string, - relatedId: string, - id: string, - type: string, - parentKey?: string, - relatedKey?: string - ): Attributes.MorphedByMany { - return new Attributes.MorphedByMany( - this, - related, - pivot, - relatedId, - id, - type, - this.localKey(parentKey), - this.relation(related).localKey(relatedKey) - ) - } - - /** - * Mutators to mutate matching fields when instantiating the model. - */ - static mutators(): Mutators { - return {} - } - - /** - * Types mapping used to dispatch entities based on their discriminator field - */ - static types(): InheritanceTypes { - return {} - } - - /** - * Get the store instance from the container. - */ - static store(): Vuex.Store { - return Container.store - } - - /** - * Get the database instance from store. - */ - static database(): Database { - return this.store().$database - } - - /** - * Create a namespaced method name for Vuex Module from the given - * method name. - */ - static namespace(method: string): string { - return `${this.database().namespace}/${this.entity}/${method}` - } - - /** - * Call Vuex Getters. - */ - static getters(method: string): any { - return this.store().getters[this.namespace(method)] - } - - /** - * Dispatch Vuex Action. - */ - static dispatch(method: string, payload?: any): Promise { - return this.store().dispatch(this.namespace(method), payload) - } - - /** - * Commit Vuex Mutation. - */ - static commit(callback: (state: State) => void) { - this.store().commit(`${this.database().namespace}/$mutate`, { - entity: this.entity, - callback - }) - } - - /** - * Get the Model schema definition from the cache. - */ - static getFields(): Fields { - if (!this.cachedFields) { - this.cachedFields = {} - } - - if (this.cachedFields[this.entity]) { - return this.cachedFields[this.entity] - } - - this.cachedFields[this.entity] = this.fields() - - return this.cachedFields[this.entity] - } - - /** - * Get all records. - */ - static all(this: T): Collection> { - return this.getters('all')() - } - - /** - * Find a record. - */ - static find( - this: T, - id: string | number | (number | string)[] - ): Item> { - return this.getters('find')(id) - } - - /** - * Get the record of the given array of ids. - */ - static findIn( - this: T, - idList: (number | string | (number | string)[])[] - ): Collection> { - return this.getters('findIn')(idList) - } - - /** - * Get query instance. - */ - static query(this: M): Query> { - return new Query(this.store(), this.entity) - } - - /** - * Check wether the associated database contains data. - */ - static exists(this: T): boolean { - return this.query().exists() - } - - /** - * Create new data with all fields filled by default values. - */ - static new(this: T): Promise> { - return this.query().new() - } - - /** - * Save given data to the store by replacing all existing records in the - * store. If you want to save data without replacing existing records, - * use the `insert` method instead. - */ - static create( - data: Record | Record[] | InsertObject, - options?: QueryPersistOptions - ): Promise { - const { data: d, options: o } = normalizeInsertPayload(data, options) - - return this.query().create(d, o) - } - - /** - * Insert records. - */ - static insert( - data: Record | Record[] | InsertObject, - options?: QueryPersistOptions - ): Promise { - const { data: d, options: o } = normalizeInsertPayload(data, options) - - return this.query().insert(d, o) - } - - /** - * Update records. - */ - static update( - this: T, - payload: Payloads.Update, - options?: PersistOptions.Update - ): Promise { - return this.dispatch('update', PayloadBuilder.normalize(payload, options)) - } - - /** - * Insert or update records. - */ - static insertOrUpdate( - this: T, - payload: Payloads.InsertOrUpdate, - options?: PersistOptions.InsertOrUpdate - ): Promise { - return this.dispatch( - 'insertOrUpdate', - PayloadBuilder.normalize(payload, options) - ) - } - - /** - * Delete records that matches the given condition. - */ - static delete( - this: M, - id: string | number | (number | string)[] - ): Promise>> - static delete( - this: M, - condition: Predicate> - ): Promise>> - static delete(this: M, payload: any): any { - return this.dispatch('delete', payload) - } - - /** - * Delete all records from the store. - */ - static deleteAll( - this: M - ): Promise>> { - return this.dispatch('deleteAll') - } - - /** - * Check if the given key is the primary key. If the model has composite - * primary key, this method is going to check if the given key is included - * in the composite key. - */ - static isPrimaryKey(key: string): boolean { - if (!Utils.isArray(this.primaryKey)) { - return this.primaryKey === key - } - - return this.primaryKey.includes(key) - } - - /** - * Check if the primary key is a composite key. - */ - static isCompositePrimaryKey(): boolean { - return Utils.isArray(this.primaryKey) - } - - /** - * Get the id (value of primary key) from teh given record. If primary key is - * not present, or it is invalid primary key value, which is other than - * `string` or `number`, it's going to return `null`. - * - * If the model has composite key, it's going to return array of ids. If any - * composite key missing, it will return `null`. - */ - static getIdFromRecord( - record: Record - ): string | number | (string | number)[] | null { - const key = this.primaryKey - - if (typeof key === 'string') { - return this.getIdFromValue(record[key]) - } - - const ids = key.reduce<(string | number)[]>((keys, k) => { - const id = this.getIdFromValue(record[k]) - - id !== null && keys.push(id) - - return keys - }, []) - - return ids.length === key.length ? ids : null - } - - /** - * Get correct index id, which is `string` | `number`, from the given value. - */ - static getIdFromValue(value: any): string | number | null { - if (typeof value === 'string' && value !== '') { - return value - } - - if (typeof value === 'number') { - return value - } - - return null - } - - /** - * Get the index ID value from the given record. An index ID is a value that - * used as a key for records within the Vuex Store. - * - * Most of the time, it's same as the value for the Model's primary key but - * it's always `string`, even if the primary key value is `number`. - * - * If the Model has a composite primary key, each value corresponding to - * those primary key will be stringified and become a single string value - * such as `'[1,2]'`. - * - * If the primary key is not present at the given record, it returns `null`. - * For the composite primary key, every key must exist at a given record, - * or it will return `null`. - */ - static getIndexIdFromRecord(record: Record): string | null { - const id = this.getIdFromRecord(record) - - if (id === null) { - return null - } - - if (Utils.isArray(id)) { - return JSON.stringify(id) - } - - return String(id) - } - - /** - * Get local key to pass to the attributes. - */ - static localKey(key?: string): string { - if (key) { - return key - } - - return typeof this.primaryKey === 'string' ? this.primaryKey : 'id' - } - - /** - * Get the model object that matches the given record type. The method is for - * getting the correct model object when the model has any inheritance - * children model. - * - * For example, if a User Model have `static types()` declared, and if you - * pass record with `{ type: 'admin' }`, then the method will likely to - * return SuperUser class. - */ - static getModelFromRecord(record: Record | Model): typeof Model | null { - // If the given record is already a model instance, return the - // model object. - if (record instanceof this) { - return record.$self() - } - - // Else, get the corresponding model for the type value if there's any. - return this.getTypeModel(record[this.typeKey]) - } - - /** - * Get a model from the container. - */ - static relation(model: typeof Model | string): typeof Model { - if (typeof model !== 'string') { - return model - } - - return this.database().model(model) - } - - /** - * Get all `belongsToMany` fields from the schema. - */ - static pivotFields(): { - [key: string]: - | Attributes.BelongsToMany - | Attributes.MorphToMany - | Attributes.MorphedByMany - }[] { - const fields: { - [key: string]: - | Attributes.BelongsToMany - | Attributes.MorphToMany - | Attributes.MorphedByMany - }[] = [] - - Utils.forOwn(this.getFields(), (field, key) => { - if ( - field instanceof Attributes.BelongsToMany || - field instanceof Attributes.MorphToMany || - field instanceof Attributes.MorphedByMany - ) { - fields.push({ [key]: field }) - } - }) - - return fields - } - - /** - * Check if fields contains the `belongsToMany` field type. - */ - static hasPivotFields(): boolean { - return this.pivotFields().length > 0 - } - - /** - * Check if the current model has a type definition - */ - static hasTypes(): boolean { - return Object.keys(this.types()).length > 0 - } - - /** - * Get the model corresponding to the given type name. If it can't be found, - * it'll return `null`. - */ - static getTypeModel(name: string): typeof Model | null { - const model = this.types()[name] - - if (!model) { - return null - } - - return model - } - - /** - * Given a Model, this returns the corresponding key in the InheritanceTypes mapping - */ - static getTypeKeyValueFromModel(model?: typeof Model): string | null { - const modelToCheck = model || this - const types = this.types() - for (const type in types) { - if (types[type].entity === modelToCheck.entity) { - return type - } - } - return null - } - - /** - * Tries to find a Relation field in all types defined in the InheritanceTypes mapping - */ - static findRelationInSubTypes( - relationName: string - ): Attributes.Relation | null { - const types = this.types() - - for (const type in types) { - const fields = types[type].getFields() - - for (const fieldName in fields) { - if ( - fieldName === relationName && - fields[fieldName] instanceof Attributes.Relation - ) { - return fields[fieldName] as Attributes.Relation - } - } - } - - return null - } - - /** - * Fill any missing fields in the given record with the default value defined - * in the model schema. - */ - static hydrate(record?: Record): Record { - return new this(record).$getAttributes() - } - - /** - * Get the constructor of this model. - */ - $self(): typeof Model { - return this.constructor as typeof Model - } - - /** - * Get the primary key for the model. - */ - $primaryKey(): string | string[] { - return this.$self().primaryKey - } - - /** - * The definition of the fields of the model and its relations. - */ - $fields(): Fields { - return this.$self().getFields() - } - - /** - * Set index id. - */ - $setIndexId(id: string | null): this { - this.$id = id - - return this - } - - /** - * Get the store instance from the container. - */ - $store(): Vuex.Store { - return this.$self().store() - } - - /** - * Create a namespaced method name for Vuex Module from the given - * method name. - */ - $namespace(method: string): string { - return this.$self().namespace(method) - } - - /** - * Call Vuex Getters. - */ - $getters(method: string): any { - return this.$self().getters(method) - } - - /** - * Dispatch Vuex Action. - */ - async $dispatch(method: string, payload?: any): Promise { - return this.$self().dispatch(method, payload) - } - - /** - * Get all records. - */ - $all(this: T): Collection { - return this.$getters('all')() - } - - /** - * Find a record. - */ - $find( - this: T, - id: string | number | (number | string)[] - ): Item { - return this.$getters('find')(id) - } - - /** - * Find record of the given array of ids. - */ - $findIn( - this: T, - idList: (number | string | (number | string)[])[] - ): Collection { - return this.$getters('findIn')(idList) - } - - /** - * Get query instance. - */ - $query(): Query { - return this.$getters('query')() - } - - /** - * Create records. - */ - async $create( - payload: Payloads.Create, - options?: PersistOptions.Create - ): Promise { - return this.$dispatch('create', PayloadBuilder.normalize(payload, options)) - } - - /** - * Insert records. - */ - async $insert( - payload: Payloads.Insert, - options?: PersistOptions.Insert - ): Promise { - return this.$dispatch('insert', PayloadBuilder.normalize(payload, options)) - } - - /** - * Update records. - */ - async $update( - payload: Payloads.Update, - options?: PersistOptions.Update - ): Promise { - if (Utils.isArray(payload)) { - return this.$dispatch( - 'update', - PayloadBuilder.normalize(payload, options) - ) - } - - if ( - payload.where !== undefined || - (options && options.where !== undefined) - ) { - return this.$dispatch( - 'update', - PayloadBuilder.normalize(payload, options) - ) - } - - if (this.$self().getIndexIdFromRecord(payload) === null) { - /* istanbul ignore else */ - if (!options) { - options = {} - } - - options.where = this.$self().getIdFromRecord(this) - - return this.$dispatch( - 'update', - PayloadBuilder.normalize(payload, options) - ) - } - - return this.$dispatch('update', PayloadBuilder.normalize(payload, options)) - } - - /** - * Insert or update records. - */ - async $insertOrUpdate( - payload: Payloads.InsertOrUpdate, - options?: PersistOptions.InsertOrUpdate - ): Promise { - return this.$dispatch( - 'insertOrUpdate', - PayloadBuilder.normalize(payload, options) - ) - } - - /** - * Save record. - */ - async $save(this: T): Promise> { - const fields = this.$self().getFields() - - const record = Object.keys(fields).reduce((record, key) => { - if (fields[key] instanceof Attributes.Type) { - record[key] = this[key] - } - - return record - }, {} as Record) - - const records = await this.$dispatch('insertOrUpdate', { data: record }) - - this.$fill(records[this.$self().entity][0]) - - return this - } - - /** - * Delete records that matches the given condition. - */ - async $delete(): Promise> { - const primaryKey = this.$primaryKey() - - if (!Utils.isArray(primaryKey)) { - return this.$dispatch('delete', this[primaryKey]) - } - - return this.$dispatch('delete', (model: this): boolean => { - return primaryKey.every((id) => model[id] === this[id]) - }) - } - - /** - * Delete all records. - */ - async $deleteAll(): Promise> { - return this.$dispatch('deleteAll') - } - - /** - * Fill the model instance with the given record. If no record were passed, - * or if the record has any missing fields, each value of the fields will - * be filled with its default value defined at model fields definition. - */ - $fill(record: Record = {}): void { - const fields = this.$fields() - - for (const key in fields) { - const field = fields[key] - const value = record[key] - - this[key] = field.make(value, record, key) - } - - // If the record contains index id, set it to the model. - record.$id !== undefined && this.$setIndexId(record.$id) - } - - /** - * Generate missing primary ids and index id. - */ - $generateId(): this { - return this.$generatePrimaryId().$generateIndexId() - } - - /** - * Generate any missing primary ids. - */ - $generatePrimaryId(): this { - const key = this.$self().primaryKey - const keys = Utils.isArray(key) ? key : [key] - - keys.forEach((k) => { - if (this[k] === undefined || this[k] === null) { - this[k] = Uid.make() - } - }) - - return this - } - - /** - * Generate index id from current model attributes. - */ - $generateIndexId(): this { - return this.$setIndexId(this.$getIndexIdFromAttributes()) - } - - /** - * Get index id based on current model attributes. - */ - $getIndexIdFromAttributes(): string | null { - return this.$self().getIndexIdFromRecord(this) - } - - /** - * Get all of the current attributes on the model. It includes index id - * value as well. This method is mainly used when saving a model to - * the store. - */ - $getAttributes(): RecordWithId { - return toAttributes(this) - } - - /** - * Serialize field values into json. - */ - $toJson(): Record { - return toJson(this) - } -} diff --git a/src/model/Serialize.ts b/src/model/Serialize.ts deleted file mode 100644 index a07de0d3..00000000 --- a/src/model/Serialize.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { isArray } from '../support/Utils' -import { Record, RecordWithId } from '../data' -import Item from '../data/Item' -import Collection from '../data/Collection' -import Relation from '../attributes/relations/Relation' -import Model from './Model' - -export interface Option { - relations?: boolean -} - -const defaultOption: Option = { - relations: true -} - -/** - * Serialize the given model to attributes. This method will ignore - * relationships, and it includes the index id. - */ -export function toAttributes(model: Model): RecordWithId { - const record = toJson(model, { relations: false }) - - record.$id = model.$id - - return record as RecordWithId -} - -/** - * Serialize given model POJO. - */ -export function toJson(model: Model, option: Option = {}): Record { - option = { ...defaultOption, ...option } - - const record: Record = {} - - const fields = model.$fields() - - for (const key in fields) { - const f = fields[key] - const v = model[key] - - if (f instanceof Relation) { - record[key] = option.relations ? relation(v) : emptyRelation(v) - continue - } - - record[key] = value(model[key]) - } - - return record -} - -/** - * Serialize given value. - */ -function value(v: any): any { - if (v === null) { - return null - } - - if (isArray(v)) { - return array(v) - } - - if (typeof v === 'object') { - return object(v) - } - - return v -} - -/** - * Serialize an array into json. - */ -function array(a: any[]): any[] { - return a.map((v) => value(v)) -} - -/** - * Serialize an object into json. - */ -function object(o: object): object { - const obj = {} - - for (const key in o) { - obj[key] = value(o[key]) - } - - return obj -} - -/** - * Serialize given relation into json. - */ -function relation(relation: Item): Record | null -function relation(relation: Collection): Record[] -function relation(relation: Item | Collection): Record | Record[] | null { - if (relation === null) { - return null - } - - if (isArray(relation)) { - return relation.map((model) => model.$toJson()) - } - - return relation.$toJson() -} - -/** - * Serialize given relation into empty json. - */ -function emptyRelation(relation: Item): null -function emptyRelation(relation: Collection): [] -function emptyRelation(relation: Item | Collection): [] | null { - return isArray(relation) ? [] : null -} diff --git a/src/model/contracts/FieldCache.ts b/src/model/contracts/FieldCache.ts deleted file mode 100644 index fc1a53eb..00000000 --- a/src/model/contracts/FieldCache.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Fields from './Fields' - -export interface FieldCache { - [key: string]: Fields -} - -export default FieldCache diff --git a/src/model/contracts/Fields.ts b/src/model/contracts/Fields.ts deleted file mode 100644 index c307be3c..00000000 --- a/src/model/contracts/Fields.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Attribute from '../../attributes/Attribute' - -export interface Fields { - [key: string]: Attribute -} - -export default Fields diff --git a/src/model/contracts/InheritanceTypes.ts b/src/model/contracts/InheritanceTypes.ts deleted file mode 100644 index 8a1ba6fd..00000000 --- a/src/model/contracts/InheritanceTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Model from '../Model' - -export interface InheritanceTypes { - [key: string]: typeof Model -} - -export default InheritanceTypes diff --git a/src/model/contracts/State.ts b/src/model/contracts/State.ts deleted file mode 100644 index b866e773..00000000 --- a/src/model/contracts/State.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface State { - [key: string]: any -} - -export default State diff --git a/src/model/support/Payloads.ts b/src/model/support/Payloads.ts deleted file mode 100644 index 69332c76..00000000 --- a/src/model/support/Payloads.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { isArray } from '../../support/Utils' -import { Record } from '../../data' -import { PersistOptions } from '../../query/options' - -export interface InsertObject extends PersistOptions { - data: Record | Record[] -} - -export interface InsertPayload { - data: Record | Record[] - options?: PersistOptions -} - -/** - * Normalize the payload arguments for persist methods. It's required for - * backward compatibility for the new persist method argument structure. - * It may be deprecated and be removed in the future. - */ -export function normalizeInsertPayload( - data: Record | Record[] | InsertObject, - options?: PersistOptions -): InsertPayload { - // If the data is an array, then the data should be an array of data. - // Therefore the whole payload should be passed directly as `data`. - if (isArray(data)) { - return { data, options } - } - - // If the data doesn't have a `data` property, it can be assumed that the - // user has passed an object as the payload. Therefore the entire payload - // should be declared as `data`. - if (data.data === undefined) { - return { data, options } - } - - // If the data contains `data` property, we should extract it and pass that - // property as `data`. - return { data: data.data, options: data } -} diff --git a/src/modules/Actions.ts b/src/modules/Actions.ts deleted file mode 100644 index 82e0573e..00000000 --- a/src/modules/Actions.ts +++ /dev/null @@ -1,151 +0,0 @@ -import Item from '../data/Item' -import Collection from '../data/Collection' -import Collections from '../data/Collections' -import Model from '../model/Model' -import ActionsContract from './contracts/Actions' -import ActionContext from './contracts/ActionContext' -import * as Payloads from './payloads/Actions' -import PayloadBuilder from './support/PayloadBuilder' - -/** - * Create new data with all fields filled by default values. - */ -async function newRecord(context: ActionContext): Promise { - const state = context.state - const entity = state.$name - - return context.dispatch( - `${state.$connection}/new`, - { entity }, - { root: true } - ) -} - -/** - * Save given data to the store by replacing all existing records in the - * store. If you want to save data without replacing existing records, - * use the `insert` method instead. - */ -async function create( - context: ActionContext, - payload: Payloads.Create -): Promise { - const state = context.state - const entity = state.$name - const data = PayloadBuilder.createPersistPayload(payload) - - return context.dispatch( - `${state.$connection}/create`, - { entity, ...data }, - { root: true } - ) -} - -/** - * Insert given data to the state. Unlike `create`, this method will not - * remove existing data within the state, but it will update the data - * with the same primary key. - */ -async function insert( - context: ActionContext, - payload: Payloads.Insert -): Promise { - const state = context.state - const entity = state.$name - const data = PayloadBuilder.createPersistPayload(payload) - - return context.dispatch( - `${state.$connection}/insert`, - { entity, ...data }, - { root: true } - ) -} - -/** - * Update data in the store. - */ -async function update( - context: ActionContext, - payload: Payloads.Update -): Promise { - const state = context.state - const entity = state.$name - const data = PayloadBuilder.createPersistPayload(payload) - - return context.dispatch( - `${state.$connection}/update`, - { entity, ...data }, - { root: true } - ) -} - -/** - * Insert or update given data to the state. Unlike `insert`, this method - * will not replace existing data within the state, but it will update only - * the submitted data with the same primary key. - */ -async function insertOrUpdate( - context: ActionContext, - payload: Payloads.InsertOrUpdate -): Promise { - const state = context.state - const entity = state.$name - const data = PayloadBuilder.createPersistPayload(payload) - - return context.dispatch( - `${state.$connection}/insertOrUpdate`, - { entity, ...data }, - { root: true } - ) -} - -/** - * Delete records from the store. The actual name for this action is `delete`, - * but named `destroy` here because `delete` can't be declared at this - * scope level. - */ -async function destroy( - context: ActionContext, - payload: Payloads.DeleteById -): Promise -async function destroy( - context: ActionContext, - payload: Payloads.DeleteByCondition -): Promise -async function destroy(context: ActionContext, payload: any): Promise { - const state = context.state - const entity = state.$name - const where = payload - - return context.dispatch( - `${state.$connection}/delete`, - { entity, where }, - { root: true } - ) -} - -/** - * Delete all data from the store. - */ -async function deleteAll(context: ActionContext): Promise { - const state = context.state - const entity = state.$name - - return context.dispatch( - `${state.$connection}/deleteAll`, - { entity }, - { root: true } - ) -} - -const Actions: ActionsContract = { - new: newRecord, - create, - insert, - update, - insertOrUpdate, - delete: destroy, - deleteAll -} - -export default Actions diff --git a/src/modules/Getters.ts b/src/modules/Getters.ts deleted file mode 100644 index adf0a75e..00000000 --- a/src/modules/Getters.ts +++ /dev/null @@ -1,63 +0,0 @@ -import Item from '../data/Item' -import Collection from '../data/Collection' -import Query from '../query/Query' -import State from './contracts/State' -import RootState from './contracts/RootState' -import GettersContract from './contracts/Getters' - -/** - * Create a new Query instance. - */ -const query = ( - state: State, - _getters: any, - _rootState: RootState, - rootGetters: any -) => (): Query => { - return rootGetters[`${state.$connection}/query`](state.$name) -} - -/** - * Get all data of given entity. - */ -const all = ( - state: State, - _getters: any, - _rootState: RootState, - rootGetters: any -) => (): Collection => { - return rootGetters[`${state.$connection}/all`](state.$name) -} - -/** - * Find a data of the given entity by given id. - */ -const find = ( - state: State, - _getters: any, - _rootState: RootState, - rootGetters: any -) => (id: string | number | (number | string)[]): Item => { - return rootGetters[`${state.$connection}/find`](state.$name, id) -} - -/** - * Find array of data of the given entity by given ids. - */ -const findIn = ( - state: State, - _getters: any, - _rootState: RootState, - rootGetters: any -) => (idList: Array): Collection => { - return rootGetters[`${state.$connection}/findIn`](state.$name, idList) -} - -const Getters: GettersContract = { - query, - all, - find, - findIn -} - -export default Getters diff --git a/src/modules/RootActions.ts b/src/modules/RootActions.ts deleted file mode 100644 index 875f001a..00000000 --- a/src/modules/RootActions.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Store } from 'vuex' -import Item from '../data/Item' -import Collection from '../data/Collection' -import Collections from '../data/Collections' -import Model from '../model/Model' -import Query from '../query/Query' -import ActionsContract from './contracts/RootActions' -import ActionContext from './contracts/RootActionContext' -import OptionsBuilder from './support/OptionsBuilder' -import * as Payloads from './payloads/RootActions' - -/** - * Create new data with all fields filled by default values. - */ -async function newRecord( - this: Store, - _context: ActionContext, - payload: Payloads.New -): Promise { - return new Query(this, payload.entity).new() -} - -/** - * Save given data to the store by replacing all existing records in the - * store. If you want to save data without replacing existing records, - * use the `insert` method instead. - */ -async function create( - this: Store, - _context: ActionContext, - payload: Payloads.Create -): Promise { - const entity = payload.entity - const data = payload.data - const options = OptionsBuilder.createPersistOptions(payload) - - return new Query(this, entity).create(data, options) -} - -/** - * Insert given data to the state. Unlike `create`, this method will not - * remove existing data within the state, but it will update the data - * with the same primary key. - */ -async function insert( - this: Store, - _context: ActionContext, - payload: Payloads.Insert -): Promise { - const entity = payload.entity - const data = payload.data - const options = OptionsBuilder.createPersistOptions(payload) - - return new Query(this, entity).insert(data, options) -} - -/** - * Update data in the store. - */ -async function update( - this: Store, - _context: ActionContext, - payload: Payloads.Update -): Promise { - const entity = payload.entity - const data = payload.data - const where = payload.where || null - const options = OptionsBuilder.createPersistOptions(payload) - - return new Query(this, entity).update(data, where, options) -} - -/** - * Insert or update given data to the state. Unlike `insert`, this method - * will not replace existing data within the state, but it will update only - * the submitted data with the same primary key. - */ -async function insertOrUpdate( - this: Store, - _context: ActionContext, - payload: Payloads.InsertOrUpdate -): Promise { - const entity = payload.entity - const data = payload.data - const options = OptionsBuilder.createPersistOptions(payload) - - return new Query(this, entity).insertOrUpdate(data, options) -} - -/** - * Delete records from the store. The actual name for this action is `delete`, - * but named `destroy` here because `delete` can't be declared at this - * scope level. - */ -async function destroy( - this: Store, - _context: ActionContext, - payload: Payloads.DeleteById -): Promise -async function destroy( - this: Store, - _context: ActionContext, - payload: Payloads.DeleteByCondition -): Promise -async function destroy( - this: Store, - _context: ActionContext, - payload: any -): Promise { - const { entity, where } = payload - - return new Query(this, entity).delete(where) -} - -/** - * Delete all data from the store. - */ -async function deleteAll( - this: Store, - _context: ActionContext, - payload?: Payloads.DeleteAll -): Promise { - if (payload && payload.entity) { - new Query(this, payload.entity).deleteAll() - - return - } - - Query.deleteAll(this) -} - -const RootActions: ActionsContract = { - new: newRecord, - create, - insert, - update, - insertOrUpdate, - delete: destroy, - deleteAll -} - -export default RootActions diff --git a/src/modules/RootGetters.ts b/src/modules/RootGetters.ts deleted file mode 100644 index 84270d01..00000000 --- a/src/modules/RootGetters.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Store } from 'vuex' -import Item from '../data/Item' -import Collection from '../data/Collection' -import Query from '../query/Query' -import RootState from './contracts/RootState' -import GettersContract from './contracts/RootGetters' - -/** - * Create a new Query instance. - */ -function query(this: Store, _state: RootState) { - return (entity: string): Query => new Query(this, entity) -} - -/** - * Get all data of given entity. - */ -function all(this: Store, _state: RootState) { - return (entity: string): Collection => new Query(this, entity).all() -} - -/** - * Find a data of the given entity by given id. - */ -function find(this: Store, _state: RootState) { - return (entity: string, id: string | number | Array): Item => { - return new Query(this, entity).find(id) - } -} - -/** - * Find a data of the given entity by given id. - */ -function findIn(this: Store, _state: RootState) { - return ( - entity: string, - idList: Array> - ): Item[] => { - return new Query(this, entity).findIn(idList) - } -} - -const RootGetters: GettersContract = { - query, - all, - find, - findIn -} - -export default RootGetters diff --git a/src/modules/RootMutations.ts b/src/modules/RootMutations.ts deleted file mode 100644 index dc02ca3b..00000000 --- a/src/modules/RootMutations.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Store } from 'vuex' -import Connection from '../connections/Connection' -import State from './contracts/RootState' -import MutationsContract from './contracts/RootMutations' -import * as Payloads from './payloads/RootMutations' - -/** - * Execute generic mutation. This method is used by `Model.commit` method so - * that user can commit any state changes easily through models. - */ -function $mutate( - this: Store, - state: State, - payload: Payloads.$Mutate -): void { - payload.callback(state[payload.entity]) -} - -/** - * Commit the given records by `new` method. - */ -function newRecord(state: State, payload: Payloads.New): void { - const entity = state[payload.entity] - entity.data = { ...entity.data, ...payload.data } -} - -/** - * Commit the given records by `insert` method. - */ -function insert(state: State, payload: Payloads.Insert): void { - const entity = state[payload.entity] - entity.data = { ...entity.data, ...payload.data } -} - -/** - * Insert the given records. - */ -function insertRecords(this: Store, state: State, payload: any): void { - const { entity, records } = payload - new Connection(this, state.$name, entity).insertRecords(records) -} - -/** - * Delete records from the store. The actual name for this mutation is - * `delete`, but named `destroy` here because `delete` can't be declared at - * this scope level. - */ -function destroy(this: Store, state: State, payload: any): void { - const { entity, id } = payload - new Connection(this, state.$name, entity).delete(id) -} - -const RootMutations: MutationsContract = { - $mutate, - new: newRecord, - insert, - insertRecords, - delete: destroy -} - -export default RootMutations diff --git a/src/modules/contracts/ActionContext.ts b/src/modules/contracts/ActionContext.ts deleted file mode 100644 index 1d34fb98..00000000 --- a/src/modules/contracts/ActionContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import State from './State' - -export type ActionContext = Vuex.ActionContext - -export default ActionContext diff --git a/src/modules/contracts/Actions.ts b/src/modules/contracts/Actions.ts deleted file mode 100644 index 1e2d2bf5..00000000 --- a/src/modules/contracts/Actions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import State from './State' - -export type Actions = Vuex.ActionTree - -export default Actions diff --git a/src/modules/contracts/Getters.ts b/src/modules/contracts/Getters.ts deleted file mode 100644 index 4d0624b6..00000000 --- a/src/modules/contracts/Getters.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import State from './State' - -export type Getters = Vuex.GetterTree - -export default Getters diff --git a/src/modules/contracts/Module.ts b/src/modules/contracts/Module.ts deleted file mode 100644 index 79c9d35d..00000000 --- a/src/modules/contracts/Module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as Vuex from 'vuex' -import RootState from './RootState' - -interface VuexModule extends Vuex.Module { - namespaced: boolean - state: () => RootState - getters: Vuex.GetterTree - actions: Vuex.ActionTree - mutations: Vuex.MutationTree - modules: Vuex.ModuleTree -} - -export default VuexModule diff --git a/src/modules/contracts/RootActionContext.ts b/src/modules/contracts/RootActionContext.ts deleted file mode 100644 index bc7adee6..00000000 --- a/src/modules/contracts/RootActionContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import RootState from './RootState' - -export type RootActionContext = Vuex.ActionContext - -export default RootActionContext diff --git a/src/modules/contracts/RootActions.ts b/src/modules/contracts/RootActions.ts deleted file mode 100644 index ee9e57bb..00000000 --- a/src/modules/contracts/RootActions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import RootState from './RootState' - -export type RootActions = Vuex.ActionTree - -export default RootActions diff --git a/src/modules/contracts/RootGetters.ts b/src/modules/contracts/RootGetters.ts deleted file mode 100644 index 3b0ed5ef..00000000 --- a/src/modules/contracts/RootGetters.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import RootState from './RootState' - -export type RootGetters = Vuex.GetterTree - -export default RootGetters diff --git a/src/modules/contracts/RootMutations.ts b/src/modules/contracts/RootMutations.ts deleted file mode 100644 index 8a2308bc..00000000 --- a/src/modules/contracts/RootMutations.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Vuex from 'vuex' -import RootState from './RootState' - -export type RootMutations = Vuex.MutationTree - -export default RootMutations diff --git a/src/modules/contracts/RootState.ts b/src/modules/contracts/RootState.ts deleted file mode 100644 index e7971157..00000000 --- a/src/modules/contracts/RootState.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RootState { - $name: string -} - -export default RootState diff --git a/src/modules/contracts/State.ts b/src/modules/contracts/State.ts deleted file mode 100644 index 5f4a184e..00000000 --- a/src/modules/contracts/State.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Record from '../../data/Record' - -export interface State { - $connection: string - $name: string - data: Record - [key: string]: any -} - -export default State diff --git a/src/modules/options/Actions.ts b/src/modules/options/Actions.ts deleted file mode 100644 index 4629e0db..00000000 --- a/src/modules/options/Actions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Condition } from '../payloads/Actions' -import PrimaryKey from '../../query/options/PrimaryKey' -import PersistOptions from '../../query/options/PersistOptions' - -export type Create = PersistOptions - -export type Insert = PersistOptions - -export interface Update extends PersistOptions { - where?: PrimaryKey | Condition | null -} - -export type InsertOrUpdate = PersistOptions diff --git a/src/modules/payloads/Actions.ts b/src/modules/payloads/Actions.ts deleted file mode 100644 index 5cc0e997..00000000 --- a/src/modules/payloads/Actions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Record } from '../../data' -import Predicate from '../../query/contracts/Predicate' -import PersistOptions from '../../query/options/PersistOptions' - -export type Condition = (record: Record) => boolean - -export type Create = CreateObject | Record[] - -export interface CreateObject extends PersistOptions { - data?: Record | Record[] - [key: string]: any -} - -export type Insert = InsertObject | Record[] - -export interface InsertObject extends PersistOptions { - data?: Record | Record[] - [key: string]: any -} - -export type Update = UpdateObject | Record[] - -export interface UpdateObject extends PersistOptions { - data?: Record | Record[] - where?: string | number | Condition - [key: string]: any -} - -export type InsertOrUpdate = InsertOrUpdateObject | Record[] - -export interface InsertOrUpdateObject extends PersistOptions { - data?: Record | Record[] - [key: string]: any -} - -export type DeleteById = string | number | (number | string)[] - -export type DeleteByCondition = Predicate diff --git a/src/modules/payloads/RootActions.ts b/src/modules/payloads/RootActions.ts deleted file mode 100644 index 432b82c5..00000000 --- a/src/modules/payloads/RootActions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Record } from '../../data' -import Predicate from '../../query/contracts/Predicate' -import PersistOptions from '../../query/options/PersistOptions' - -export type Condition = (record: Record) => boolean - -export interface New { - entity: string -} - -export interface Create extends PersistOptions { - entity: string - data: Record | Record[] -} - -export interface Insert extends PersistOptions { - entity: string - data: Record | Record[] -} - -export interface Update extends PersistOptions { - entity: string - data: Record | Record[] - where?: string | number | Condition -} - -export interface InsertOrUpdate extends PersistOptions { - entity: string - data: Record | Record[] -} - -export interface DeleteById { - entity: string - where: string | number -} - -export interface DeleteByCondition { - entity: string - where: Predicate -} - -export interface DeleteAll { - entity: string -} diff --git a/src/modules/payloads/RootMutations.ts b/src/modules/payloads/RootMutations.ts deleted file mode 100644 index e9ec3684..00000000 --- a/src/modules/payloads/RootMutations.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Record, Records } from '../../data' -import Predicate from '../../query/contracts/Predicate' -import PersistOptions from '../../query/options/PersistOptions' -import State from '../contracts/State' - -export type Condition = (record: Record) => boolean - -export interface $Mutate { - entity: string - callback: (state: State) => void -} - -export interface New { - entity: string - data: Records -} - -export interface Insert { - entity: string - data: Records -} - -export interface Update extends PersistOptions { - entity: string - data: Record | Record[] - where?: string | number | Condition -} - -export interface InsertOrUpdate extends PersistOptions { - entity: string - data: Record | Record[] -} - -export interface Delete { - entity: string - where: string | number | Predicate -} - -export interface DeleteAll { - entity: string -} diff --git a/src/modules/support/OptionsBuilder.ts b/src/modules/support/OptionsBuilder.ts deleted file mode 100644 index 7d5c8d3f..00000000 --- a/src/modules/support/OptionsBuilder.ts +++ /dev/null @@ -1,15 +0,0 @@ -import PersistOptions from '../../query/options/PersistOptions' - -export default class OptionsBuilder { - /** - * Get persist options from the given payload. - */ - static createPersistOptions(payload: PersistOptions): PersistOptions { - return { - create: payload.create, - insert: payload.insert, - update: payload.update, - insertOrUpdate: payload.insertOrUpdate - } - } -} diff --git a/src/modules/support/PayloadBuilder.ts b/src/modules/support/PayloadBuilder.ts deleted file mode 100644 index 6a8e4e55..00000000 --- a/src/modules/support/PayloadBuilder.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Create, Insert, Update, InsertOrUpdate } from '../payloads/Actions' -import OptionsBuilder from './OptionsBuilder' - -export type PersistPayload = Create | Insert | Update | InsertOrUpdate - -export default class PayloadBuilder { - /** - * Create downstream payload from input data to determine the style of data - * being provided for downward compatibility. - */ - static createPersistPayload(payload: PersistPayload): PersistPayload { - // If the payload is an array, then the payload should be an array of - // data therefore the whole payload should be declared as `data`. - if (Array.isArray(payload)) { - return { data: payload } - } - - // If the payload doesn't have a `data` property, it can be assumed that - // the user has passed an object as the payload therefore the entire - // payload should be declared as `data` - if (payload.data === undefined) { - return { data: payload } - } - - // It can safely be assumed the user is providing payload with `data` intact. - return payload - } - - /** - * Normalize persist payload by converting the new style of persisting data - * through method arguments, while maintaining existing style (data key), - * to pass on to the module API. - */ - static normalize(payload?: any, options?: any): PersistPayload { - const persistPayload = this.createPersistPayload(payload) - const persistOptions = OptionsBuilder.createPersistOptions(payload) - - return { - ...persistPayload, - ...persistOptions, - ...options - } - } -} diff --git a/src/plugins/use.ts b/src/plugins/use.ts deleted file mode 100644 index 8058c976..00000000 --- a/src/plugins/use.ts +++ /dev/null @@ -1,112 +0,0 @@ -import Repository from '../repository/Repository' -import Model from '../model/Model' -import Attribute from '../attributes/Attribute' -import Type from '../attributes/types/Type' -import Attr from '../attributes/types/Attr' -import String from '../attributes/types/String' -import Number from '../attributes/types/Number' -import Boolean from '../attributes/types/Boolean' -import Uid from '../attributes/types/Uid' -import Relation from '../attributes/relations/Relation' -import HasOne from '../attributes/relations/HasOne' -import BelongsTo from '../attributes/relations/BelongsTo' -import HasMany from '../attributes/relations/HasMany' -import HasManyBy from '../attributes/relations/HasManyBy' -import BelongsToMany from '../attributes/relations/BelongsToMany' -import HasOneThrough from '../attributes/relations/HasOneThrough' -import HasManyThrough from '../attributes/relations/HasManyThrough' -import MorphTo from '../attributes/relations/MorphTo' -import MorphOne from '../attributes/relations/MorphOne' -import MorphMany from '../attributes/relations/MorphMany' -import MorphToMany from '../attributes/relations/MorphToMany' -import MorphedByMany from '../attributes/relations/MorphedByMany' -import Getters from '../modules/Getters' -import Actions from '../modules/Actions' -import RootGetters from '../modules/RootGetters' -import RootActions from '../modules/RootActions' -import RootMutations from '../modules/RootMutations' -import GettersContract from '../modules/contracts/Getters' -import ActionsContract from '../modules/contracts/Actions' -import RootGettersContract from '../modules/contracts/RootGetters' -import RootActionsContract from '../modules/contracts/RootActions' -import RootMutationsContract from '../modules/contracts/RootMutations' -import Query from '../query/Query' -import Database from '../database/Database' - -export interface PluginComponents { - Repository: typeof Repository - Model: typeof Model - Attribute: typeof Attribute - Type: typeof Type - Attr: typeof Attr - String: typeof String - Number: typeof Number - Boolean: typeof Boolean - Uid: typeof Uid - Relation: typeof Relation - HasOne: typeof HasOne - BelongsTo: typeof BelongsTo - HasMany: typeof HasMany - HasManyBy: typeof HasManyBy - BelongsToMany: typeof BelongsToMany - HasOneThrough: typeof HasOneThrough - HasManyThrough: typeof HasManyThrough - MorphTo: typeof MorphTo - MorphOne: typeof MorphOne - MorphMany: typeof MorphMany - MorphToMany: typeof MorphToMany - MorphedByMany: typeof MorphedByMany - Getters: GettersContract - Actions: ActionsContract - RootGetters: RootGettersContract - RootActions: RootActionsContract - RootMutations: RootMutationsContract - Query: typeof Query - Database: typeof Database -} - -export interface Options { - [key: string]: any -} - -export interface Plugin { - [key: string]: any -} - -export type Use = (plugin: Plugin, options?: Options) => void - -export function use(plugin: Plugin, options: Options = {}): void { - const components: PluginComponents = { - Repository, - Model, - Attribute, - Type, - Attr, - String, - Number, - Boolean, - Uid, - Relation, - HasOne, - BelongsTo, - HasMany, - HasManyBy, - BelongsToMany, - HasOneThrough, - HasManyThrough, - MorphTo, - MorphOne, - MorphMany, - MorphToMany, - MorphedByMany, - Getters, - Actions, - RootGetters, - RootActions, - RootMutations, - Query, - Database - } - - plugin.install(components, options) -} diff --git a/src/polyfills/index.ts b/src/polyfills/index.ts deleted file mode 100644 index b505cb3d..00000000 --- a/src/polyfills/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import 'core-js/fn/array/from' -import 'core-js/fn/array/includes' -import 'core-js/fn/object/assign' -import 'core-js/fn/object/entries' -import 'core-js/fn/object/values' -import 'core-js/fn/string/starts-with' diff --git a/src/query/Query.ts b/src/query/Query.ts deleted file mode 100644 index e17fd597..00000000 --- a/src/query/Query.ts +++ /dev/null @@ -1,1441 +0,0 @@ -import { Store } from 'vuex' -import Utils from '../support/Utils' -import Database, { Models } from '../database/Database' -import Connection from '../connections/Connection' -import * as Data from '../data' -import Model from '../model/Model' -import State from '../modules/contracts/State' -import RootState from '../modules/contracts/RootState' -import * as Contracts from './contracts' -import * as Options from './options' -import Processor from './processors/Processor' -import Filter from './filters/Filter' -import Loader from './loaders/Loader' -import Rollcaller from './rollcallers/Rollcaller' - -export type UpdateClosure = (record: Data.Record) => void - -export type UpdateCondition = number | string | Contracts.Predicate | null - -export type Constraint = (query: Query) => void | boolean - -export type ConstraintCallback = (relationName: string) => Constraint | null - -export default class Query { - /** - * The global lifecycle hook registries. - */ - static hooks: Contracts.GlobalHooks = {} - - /** - * The counter to generate the UID for global hooks. - */ - static lastHookId: number = 0 - - /** - * The store instance. - */ - store: Store - - /** - * The database instance. - */ - database: Database - - /** - * The entity name being queried. - */ - entity: string - - /** - * The entity name being queried. - */ - baseEntity: string - - /** - * The entity state of the Vuex Store. - */ - state: State - - /** - * The root state of the Vuex Store. - */ - rootState: RootState - - /** - * The model being queried. - */ - model: typeof Model - - /** - * The base model. - */ - baseModel: typeof Model - - /** - * This flag lets us know if current Query instance applies to - * a base class or not (in order to know when to filter out - * some records). - */ - appliedOnBase: boolean = true - - /** - * Primary key ids to filter records by. It is used for filtering records - * direct key lookup when a user is trying to fetch records by its - * primary key. - * - * It should not be used if there is a logic which prevents index usage, for - * example, an "or" condition which already requires a full scan of records. - */ - idFilter: Set | null = null - - /** - * Whether to use `idFilter` key lookup. True if there is a logic which - * prevents index usage, for example, an "or" condition which already - * requires full scan. - */ - cancelIdFilter: boolean = false - - /** - * Primary key ids to filter joined records. It is used for filtering - * records direct key lookup. It should not be cancelled, because it - * is free from the effects of normal where methods. - */ - joinedIdFilter: Set | null = null - - /** - * The where constraints for the query. - */ - wheres: Options.Where[] = [] - - /** - * The has constraints for the query. - */ - have: Options.Has[] = [] - - /** - * The orders of the query result. - */ - orders: Options.Orders[] = [] - - /** - * Number of results to skip. - */ - offsetNumber: number = 0 - - /** - * Maximum number of records to return. - * - * We use polyfill of `Number.MAX_SAFE_INTEGER` for IE11 here. - */ - limitNumber: number = Math.pow(2, 53) - 1 - - /** - * The relationships that should be eager loaded with the result. - */ - load: Options.Load = {} - - /** - * Create a new Query instance. - */ - constructor(store: Store, entity: string) { - this.store = store - this.database = store.$database - - this.model = this.getModel(entity) - this.baseModel = this.getBaseModel(entity) - - this.entity = entity - this.baseEntity = this.baseModel.entity - - this.rootState = this.database.getState() - this.state = this.rootState[this.baseEntity] - - this.appliedOnBase = this.baseEntity === this.entity - } - - /** - * Delete all records from the store. - */ - static deleteAll(store: Store): void { - const database = store.$database - const models = database.models() - - for (const entity in models) { - const state = database.getState()[entity] - state && new this(store, entity).deleteAll() - } - } - - /** - * Register a global hook. It will return ID for the hook that users may use - * it to unregister hooks. - */ - static on(on: string, callback: Contracts.HookableClosure): number { - const id = ++this.lastHookId - - if (!this.hooks[on]) { - this.hooks[on] = [] - } - - this.hooks[on].push({ id, callback }) - - return id - } - - /** - * Unregister global hook with the given id. - */ - static off(id: number): boolean { - return Object.keys(this.hooks).some((on) => { - const hooks = this.hooks[on] - - const index = hooks.findIndex((h) => h.id === id) - - if (index === -1) { - return false - } - - hooks.splice(index, 1) - - return true - }) - } - - /** - * Get query class. - */ - self(): typeof Query { - return this.constructor as typeof Query - } - - /** - * Create a new query instance. - */ - newQuery(entity?: string): Query { - entity = entity || this.entity - - return new Query(this.store, entity) - } - - /** - * Get the new connection instance. - */ - connection(): Connection { - return new Connection( - this.store, - this.database.namespace, - this.entity - ) - } - - /** - * Get model of given name from the container. - */ - getModel(name?: string): typeof Model { - const entity = name || this.entity - - return this.database.model(entity) - } - - /** - * Get all models from the container. - */ - getModels(): Models { - return this.database.models() - } - - /** - * Get base model of given name from the container. - */ - getBaseModel(name: string): typeof Model { - return this.database.baseModel(name) - } - - /** - * Returns all record of the query chain result. This method is alias - * of the `get` method. - */ - all(): Data.Collection { - return this.get() - } - - /** - * Find the record by the given id. - */ - find(value: Options.PrimaryKey): Data.Item { - const record = this.state.data[this.normalizeIndexId(value)] - - if (!record) { - return null - } - - return this.item(this.hydrate(record)) - } - - /** - * Get the record of the given array of ids. - */ - findIn(values: Options.PrimaryKey[]): Data.Collection { - if (!Utils.isArray(values)) { - return [] - } - - const records = values.reduce>((collection, value) => { - const record = this.state.data[this.normalizeIndexId(value)] - - if (!record) { - return collection - } - - collection.push(this.hydrate(record)) - - return collection - }, []) - - return this.collect(records) - } - - /** - * Returns all record of the query chain result. - */ - get(): Data.Collection { - const records = this.select() - - return this.collect(records) - } - - /** - * Returns the first record of the query chain result. - */ - first(): Data.Item { - const records = this.select() - - if (records.length === 0) { - return null - } - - return this.item(this.hydrate(records[0])) - } - - /** - * Returns the last record of the query chain result. - */ - last(): Data.Item { - const records = this.select() - - if (records.length === 0) { - return null - } - - return this.item(this.hydrate(records[records.length - 1])) - } - - /** - * Checks whether a result of the query chain exists. - */ - exists(): boolean { - const records = this.select() - - return records.length > 0 - } - - /** - * Add a and where clause to the query. - */ - where(field: any, value?: any): this { - if (this.isIdfilterable(field)) { - this.setIdFilter(value) - } - - this.wheres.push({ field, value, boolean: 'and' }) - - return this - } - - /** - * Add a or where clause to the query. - */ - orWhere(field: any, value?: any): this { - // Cancel id filter usage, since "or" needs full scan. - this.cancelIdFilter = true - - this.wheres.push({ field, value, boolean: 'or' }) - - return this - } - - /** - * Filter records by their primary key. - */ - whereId(value: Options.PrimaryKey): this { - if (this.model.isCompositePrimaryKey()) { - return this.where('$id', this.normalizeIndexId(value)) - } - - return this.where(this.model.primaryKey, value) - } - - /** - * Filter records by their primary keys. - */ - whereIdIn(values: Options.PrimaryKey[]): this { - if (this.model.isCompositePrimaryKey()) { - const idList = values.reduce((keys, value) => { - return [...keys, this.normalizeIndexId(value)] as string[] - }, []) - - return this.where('$id', idList) - } - - return this.where(this.model.primaryKey, values) - } - - /** - * Fast comparison for foreign keys. If the foreign key is the primary key, - * it uses object lookup, fallback normal where otherwise. - * - * Why separate `whereFk` instead of just `where`? Additional logic needed - * for the distinction between where and orWhere in normal queries, but - * Fk lookups are always "and" type. - */ - whereFk(field: string, value: Options.PrimaryKey): this { - const values = Utils.isArray(value) ? value : [value] - - // If lookup filed is the primary key. Initialize or get intersection, - // because boolean and could have a condition such as - // `whereId(1).whereId(2).get()`. - if (field === this.model.primaryKey) { - this.setJoinedIdFilter(values) - - return this - } - - // Else fallback to normal where. - this.where(field, values) - - return this - } - - /** - * Convert value to string for composite primary keys as it expects an array. - * Otherwise return as is. - * - * Throws an error when malformed value is given: - * - Composite primary key defined on model, expects value to be array. - * - Normal primary key defined on model, expects a primitive value. - */ - private normalizeIndexId(value: Options.PrimaryKey): string | number { - if (this.model.isCompositePrimaryKey()) { - if (!Utils.isArray(value)) { - throw new Error( - '[Vuex ORM] Entity `' + - this.entity + - '` is configured with a composite ' + - 'primary key and expects an array value but instead received: ' + - JSON.stringify(value) - ) - } - - return JSON.stringify(value) - } - - if (Utils.isArray(value)) { - throw new Error( - '[Vuex ORM] Entity `' + - this.entity + - '` expects a single value but ' + - 'instead received: ' + - JSON.stringify(value) - ) - } - - return value - } - - /** - * Check whether the given field is filterable through primary key - * direct look up. - */ - private isIdfilterable(field: any): boolean { - return ( - (field === this.model.primaryKey || field === '$id') && - !this.cancelIdFilter - ) - } - - /** - * Set id filter for the given where condition. - */ - private setIdFilter(value: string | number | (string | number)[]): void { - const values = Utils.isArray(value) ? value : [value] - - // Initialize or get intersection, because boolean and could have a - // condition such as `whereIdIn([1,2,3]).whereIdIn([1,2]).get()`. - if (this.idFilter === null) { - this.idFilter = new Set(values) - - return - } - - this.idFilter = new Set( - values.filter((v) => (this.idFilter as Set).has(v)) - ) - } - - /** - * Set joined id filter for the given where condition. - */ - private setJoinedIdFilter(values: (string | number)[]): void { - // Initialize or get intersection, because boolean and could have a - // condition such as `whereId(1).whereId(2).get()`. - if (this.joinedIdFilter === null) { - this.joinedIdFilter = new Set(values) - - return - } - - this.joinedIdFilter = new Set( - values.filter((v) => (this.joinedIdFilter as Set).has(v)) - ) - } - - /** - * Add an order to the query. - */ - orderBy( - key: Options.OrderKey, - direction: Options.OrderDirection = 'asc' - ): this { - this.orders.push({ key, direction }) - - return this - } - - /** - * Add an offset to the query. - */ - offset(offset: number): this { - this.offsetNumber = offset - - return this - } - - /** - * Add limit to the query. - */ - limit(limit: number): this { - this.limitNumber = limit - - return this - } - - /** - * Set the relationships that should be loaded. - */ - with( - name: string | string[], - constraint: Contracts.RelationshipConstraint | null = null - ): this { - Loader.with(this, name, constraint) - - return this - } - - /** - * Query all relations. - */ - withAll(constraint: Contracts.RelationshipConstraint | null = null): this { - Loader.withAll(this, constraint) - - return this - } - - /** - * Query all relations recursively. - */ - withAllRecursive(depth: number = 3): this { - Loader.withAllRecursive(this, depth) - - return this - } - - /** - * Set where constraint based on relationship existence. - */ - has(relation: string, operator?: string | number, count?: number): this { - Rollcaller.has(this, relation, operator, count) - - return this - } - - /** - * Set where constraint based on relationship absence. - */ - hasNot(relation: string, operator?: string | number, count?: number): this { - Rollcaller.hasNot(this, relation, operator, count) - - return this - } - - /** - * Add where has condition. - */ - whereHas(relation: string, constraint: Options.HasConstraint): this { - Rollcaller.whereHas(this, relation, constraint) - - return this - } - - /** - * Add where has not condition. - */ - whereHasNot(relation: string, constraint: Options.HasConstraint): this { - Rollcaller.whereHasNot(this, relation, constraint) - - return this - } - - /** - * Get all records from the state and convert them into the array of - * model instances. - */ - records(): Data.Collection { - this.finalizeIdFilter() - - return this.getIdsToLookup().reduce>((models, id) => { - const record = this.state.data[id] - - if (!record) { - return models - } - - const model = this.hydrate(record) - - // Ignore if the model is not current type of model. - if ( - !this.appliedOnBase && - !(this.model.entity === model.$self().entity) - ) { - return models - } - - models.push(model) - - return models - }, []) - } - - /** - * Check whether if id filters should on select. If not, clear out id filter. - */ - private finalizeIdFilter(): void { - if (!this.cancelIdFilter || this.idFilter === null) { - return - } - - this.where( - this.model.isCompositePrimaryKey() ? '$id' : this.model.primaryKey, - Array.from(this.idFilter.values()) - ) - - this.idFilter = null - } - - /** - * Get a list of id that should be used to lookup when fetching records - * from the state. - */ - private getIdsToLookup(): (string | number)[] { - // If both id filter and joined id filter are set, intersect them. - if (this.idFilter && this.joinedIdFilter) { - return Array.from(this.idFilter.values()).filter((id) => { - return (this.joinedIdFilter as Set).has(id) - }) - } - - // If only either one is set, return which one is set. - if (this.idFilter || this.joinedIdFilter) { - return Array.from( - ( - this.idFilter || (this.joinedIdFilter as Set) - ).values() - ) - } - - // If none is set, return all keys. - return Object.keys(this.state.data) - } - - /** - * Process the query and filter data. - */ - select(): Data.Collection { - // At first, well apply any `has` condition to the query. - Rollcaller.applyConstraints(this) - - // Next, get all record as an array and then start filtering it through. - let records = this.records() - - // Process `beforeSelect` hook. - records = this.executeSelectHook('beforeSelect', records) - - // Let's filter the records at first by the where clauses. - records = this.filterWhere(records) - - // Process `afterWhere` hook. - records = this.executeSelectHook('afterWhere', records) - - // Next, lets sort the data. - records = this.filterOrderBy(records) - - // Process `afterOrderBy` hook. - records = this.executeSelectHook('afterOrderBy', records) - - // Finally, slice the record by limit and offset. - records = this.filterLimit(records) - - // Process `afterLimit` hook. - records = this.executeSelectHook('afterLimit', records) - - return records - } - - /** - * Filter the given data by registered where clause. - */ - filterWhere(records: Data.Collection): Data.Collection { - return Filter.where(this, records) - } - - /** - * Sort the given data by registered orders. - */ - filterOrderBy(records: Data.Collection): Data.Collection { - return Filter.orderBy(this, records) - } - - /** - * Limit the given records by the limit and offset. - */ - filterLimit(records: Data.Collection): Data.Collection { - return Filter.limit(this, records) - } - - /** - * Get the count of the retrieved data. - */ - count(): number { - return this.get().length - } - - /** - * Get the max value of the specified filed. - */ - max(field: string): number { - const numbers = this.get().reduce((numbers, item) => { - if (typeof item[field] === 'number') { - numbers.push(item[field]) - } - - return numbers - }, []) - - return numbers.length === 0 ? 0 : Math.max(...numbers) - } - - /** - * Get the min value of the specified filed. - */ - min(field: string): number { - const numbers = this.get().reduce((numbers, item) => { - if (typeof item[field] === 'number') { - numbers.push(item[field]) - } - - return numbers - }, []) - - return numbers.length === 0 ? 0 : Math.min(...numbers) - } - - /** - * Get the sum value of the specified filed. - */ - sum(field: string): number { - return this.get().reduce((sum, item) => { - if (typeof item[field] === 'number') { - sum += item[field] - } - - return sum - }, 0) - } - - /** - * Create a item from given record. - */ - item(item: Data.Instance): Data.Item { - if (Object.keys(this.load).length > 0) { - Loader.eagerLoadRelations(this, [item]) - } - - return item - } - - /** - * Create a collection (array) from given records. - */ - collect(collection: Data.Collection): Data.Collection { - if (collection.length < 1) { - return [] - } - - if (Object.keys(this.load).length > 0) { - collection = collection.map((item) => { - const model = this.model.getModelFromRecord(item) as typeof Model - - return new model(item) as T - }) - - Loader.eagerLoadRelations(this, collection) - } - - return collection - } - - /** - * Create new data with all fields filled by default values. - */ - async new(): Promise { - const model = new this.model().$generateId() as T - - this.connection().new(model.$getAttributes()) - - return model - } - - /** - * Save given data to the store by replacing all existing records in the - * store. If you want to save data without replacing existing records, - * use the `insert` method instead. - */ - create( - data: Data.Record | Data.Record[], - options?: Options.PersistOptions - ): Promise { - return this.persist('create', data, options) - } - - /** - * Create records to the state. - */ - createRecords(records: Data.Records): Data.Collection { - this.deleteAll() - - return this.insertRecords(records) - } - - /** - * Insert given data to the store. Unlike `create`, this method will not - * remove existing data within the store, but it will update the data - * with the same primary key. - */ - insert( - data: Data.Record | Data.Record[], - options?: Options.PersistOptions - ): Promise { - return this.persist('insert', data, options) - } - - /** - * Insert records to the store. - */ - insertRecords(records: Data.Records): Data.Collection { - let collection = this.mapHydrateRecords(records) - - collection = this.executeMutationHooks('beforeCreate', collection) - - this.connection().insert( - collection.map((m) => m.$getAttributes()) - ) - - this.executeMutationHooks('afterCreate', collection) - - return collection - } - - /** - * Update data in the state. - */ - update( - data: Data.Record | Data.Record[] | UpdateClosure, - condition: UpdateCondition, - options: Options.PersistOptions - ): - | Promise> - | Promise> - | Promise { - // If the data is array, simply normalize the data and update them. - if (Utils.isArray(data)) { - return this.persist('update', data, options) - } - - // OK, the data is not an array. Now let's check `data` to see what we can - // do if it's a closure. - if (typeof data === 'function') { - // If the data is closure, but if there's no condition, we wouldn't know - // what record to update so raise an error and abort. - if (!condition) { - throw new Error( - 'You must specify `where` to update records by specifying `data` as a closure.' - ) - } - - // If the condition is a closure, then update records by the closure. - if (typeof condition === 'function') { - return this.updateByCondition(data, condition) - } - - // Else the condition is either String or Number, so let's - // update the record by ID. - return this.updateById(data, condition) - } - - // Now the data is not a closure, and it's not an array, so it should be an object. - // If the condition is closure, we can't normalize the data so let's update - // records using the closure. - if (typeof condition === 'function') { - return this.updateByCondition(data, condition) - } - - // If there's no condition, let's normalize the data and update them. - if (!condition) { - return this.persist('update', data, options) - } - - // Now since the condition is either String or Number, let's check if the - // model's primary key is not a composite key. If yes, we can't set the - // condition as ID value for the record so throw an error and abort. - if (this.model.isCompositePrimaryKey() && !Utils.isArray(condition)) { - throw new Error( - "[Vuex ORM] You can't specify `where` value as `string` or `number` " + - 'when you have a composite key defined in your model. Please include ' + - 'composite keys to the `data` fields.' - ) - } - - // Finally, let's add condition as the primary key of the object and - // then normalize them to update the records. - return this.updateById(data, condition) - } - - /** - * Update all records. - */ - updateRecords(records: Data.Records): Data.Collection { - const models = this.hydrateRecordsByMerging(records) - - return this.performUpdate(models) - } - - /** - * Update the state by id. - */ - async updateById( - data: Data.Record | UpdateClosure, - id: string | number - ): Promise> { - id = typeof id === 'number' ? id.toString() : this.normalizeIndexId(id) - - const record = this.state.data[id] - - if (!record) { - return null - } - - const model = this.hydrate(record) - - const instances: Data.Instances = { - [id]: this.processUpdate(data, model) - } - - this.performUpdate(instances) - - return instances[id] as Data.Item - } - - /** - * Update the state by condition. - */ - async updateByCondition( - data: Data.Record | UpdateClosure, - condition: Contracts.Predicate - ): Promise> { - const instances = Object.keys(this.state.data).reduce>( - (instances, id) => { - const instance = this.hydrate(this.state.data[id]) - - if (!condition(instance)) { - return instances - } - - instances[id] = this.processUpdate(data, instance) - - return instances - }, - {} - ) - - return this.performUpdate(instances) - } - - /** - * Update the given record with given data. - */ - processUpdate( - data: Data.Record | UpdateClosure, - instance: Data.Instance - ): Data.Instance { - if (typeof data === 'function') { - ;(data as UpdateClosure)(instance) - - return instance - } - - // When the updated instance is not the base model, we tell te hydrate what model to use - if (instance.constructor !== this.model && instance instanceof Model) { - return this.hydrate( - { ...instance, ...data }, - instance.constructor as typeof Model - ) - } - - return this.hydrate({ ...instance, ...data }) - } - - /** - * Commit `update` to the state. - */ - private performUpdate(models: Data.Instances): Data.Collection { - models = this.updateIndexes(models) - - const beforeHooks = this.buildHooks( - 'beforeUpdate' - ) as Contracts.MutationHook[] - const afterHooks = this.buildHooks( - 'afterUpdate' - ) as Contracts.MutationHook[] - - const updated: Data.Collection = [] - - for (const id in models) { - const model = models[id] - - if ( - beforeHooks.some((hook) => hook(model, null, this.entity) === false) - ) { - continue - } - - this.commitInsert(model.$getAttributes()) - - afterHooks.forEach((hook) => { - hook(model, null, this.entity) - }) - - updated.push(model) - } - - return updated - } - - /** - * Update the key of the instances. This is needed when a user updates - * record's primary key. We must then update the index key to - * correspond with new id value. - */ - private updateIndexes(instances: Data.Instances): Data.Instances { - return Object.keys(instances).reduce>( - (instances, key) => { - const instance = instances[key] - const id = String(this.model.getIndexIdFromRecord(instance)) - - if (key !== id) { - instance.$id = id - - instances[id] = instance - - delete instances[key] - } - - return instances - }, - instances - ) - } - - /** - * Insert or update given data to the state. Unlike `insert`, this method - * will not replace existing data within the state, but it will update only - * the submitted data with the same primary key. - */ - insertOrUpdate( - data: Data.Record | Data.Record[], - options: Options.PersistOptions - ): Promise { - return this.persist('insertOrUpdate', data, options) - } - - /** - * Insert or update the records. - */ - insertOrUpdateRecords(records: Data.Records): Data.Collection { - let toBeInserted: Data.Records = {} - let toBeUpdated: Data.Records = {} - - Object.keys(records).forEach((id) => { - const record = records[id] - - if (this.state.data[id]) { - toBeUpdated[id] = record - - return - } - - toBeInserted[id] = record - }) - - return [ - ...this.insertRecords(toBeInserted), - ...this.updateRecords(toBeUpdated) - ] - } - - /** - * Persist data into the state while preserving it's original structure. - */ - async persist( - method: Options.PersistMethods, - data: Data.Record | Data.Record[], - options: Options.PersistOptions = {} - ): Promise { - const clonedData = Utils.cloneDeep(data) - - const normalizedData = this.normalize(clonedData) - - if (Utils.isEmpty(normalizedData)) { - if (method === 'create') { - this.emptyState() - } - - return {} - } - - return Object.entries(normalizedData).reduce( - (collections, [entity, records]) => { - const newQuery = this.newQuery(entity) - - const methodForEntity = this.getPersistMethod(entity, options, method) - - const collection = newQuery.persistRecords(methodForEntity, records) - - if (collection.length > 0) { - collections[entity] = collection - } - - return collections - }, - {} - ) - } - - /** - * Persist given records to the store by the given method. - */ - persistRecords( - method: Options.PersistMethods, - records: Data.Records - ): Data.Collection { - switch (method) { - case 'create': - return this.createRecords(records) - case 'insert': - return this.insertRecords(records) - case 'update': - return this.updateRecords(records) - case 'insertOrUpdate': - return this.insertOrUpdateRecords(records) - } - } - - /** - * Get persist method from given information. - */ - private getPersistMethod( - entity: string, - options: Options.PersistOptions, - fallback: Options.PersistMethods - ): Options.PersistMethods { - if (options.create && options.create.includes(entity)) { - return 'create' - } - - if (options.insert && options.insert.includes(entity)) { - return 'insert' - } - - if (options.update && options.update.includes(entity)) { - return 'update' - } - - if (options.insertOrUpdate && options.insertOrUpdate.includes(entity)) { - return 'insertOrUpdate' - } - - return fallback - } - - /** - * Delete matching records with the given condition from the store. - */ - delete(condition: string | number | (number | string)[]): Data.Item - delete(condition: Contracts.Predicate): Data.Collection - delete(condition: any): any { - if (typeof condition === 'function') { - return this.deleteByCondition(condition) - } - - return this.deleteById(condition) - } - - /** - * Delete all records from the store. Even when deleting all records, we'll - * iterate over all records to ensure that before and after hook will be - * called for each existing records. - */ - deleteAll(): Data.Collection { - // If the target entity is the base entity and not inherited entity, we can - // just delete all records. - if (this.appliedOnBase) { - return this.deleteByCondition(() => true) - } - - // Otherwise, we should filter out any derived entities from being deleted - // so we'll add such filter here. - return this.deleteByCondition( - (model) => model.$self().entity === this.model.entity - ) - } - - /** - * Delete a record from the store by given id. - */ - private deleteById(id: string | number | (number | string)[]): Data.Item { - const item = this.find(id) - - if (!item) { - return null - } - - return this.deleteByCondition((model) => model.$id === item.$id)[0] - } - - /** - * Perform the actual delete query to the store. - */ - private deleteByCondition( - condition: Contracts.Predicate - ): Data.Collection { - let collection = this.mapHydrateAndFilterRecords(this.state.data, condition) - - collection = this.executeMutationHooks('beforeDelete', collection) - - if (collection.length === 0) { - return [] - } - - this.commitDelete(collection.map((model) => model.$id as string)) - - this.executeMutationHooks('afterDelete', collection) - - return collection - } - - /** - * Commit mutation. - */ - private commit(name: string, payload: any): void { - this.store.commit(`${this.database.namespace}/${name}`, { - entity: this.baseEntity, - ...payload - }) - } - - /** - * Commit insert mutation. - */ - private commitInsert(record: Data.Record): void { - this.commit('insert', { record }) - } - - /** - * Commit delete mutation. - */ - private commitDelete(id: string[]): void { - this.commit('delete', { id }) - } - - /** - * Normalize the given data. - */ - normalize(data: Data.Record | Data.Record[]): Data.NormalizedData { - return Processor.normalize(this, data) - } - - /** - * Convert given record to the model instance. - */ - hydrate(record: Data.Record, forceModel?: typeof Model): Data.Instance { - if (forceModel) { - return new forceModel(record) as T - } - - const newModel = this.model.getModelFromRecord(record) - - if (newModel !== null) { - return new newModel(record) as T - } - - if (!this.appliedOnBase && record[this.model.typeKey] === undefined) { - const typeValue = this.model.getTypeKeyValueFromModel() - - record = { ...record, [this.model.typeKey]: typeValue } - - return new this.model(record) as T - } - - const baseModel = this.getBaseModel(this.entity) - - return new baseModel(record) as T - } - - /** - * Convert given records to instances by merging existing record. If there's - * no existing record, that record will not be included in the result. - */ - hydrateRecordsByMerging(records: Data.Records): Data.Instances { - return Object.keys(records).reduce>((instances, id) => { - const recordInStore = this.state.data[id] - - if (!recordInStore) { - return instances - } - - const record = records[id] - - const modelForRecordInStore = this.model.getModelFromRecord(recordInStore) - - if (modelForRecordInStore === null) { - instances[id] = this.hydrate({ ...recordInStore, ...record }) - - return instances - } - - instances[id] = this.hydrate( - { ...recordInStore, ...record }, - modelForRecordInStore - ) - - return instances - }, {}) - } - - /** - * Convert all given records and return it as a collection. - */ - private mapHydrateRecords(records: Data.Records): Data.Collection { - return Utils.map(records, (record) => this.hydrate(record)) - } - - /** - * Convert all given records and return it as a collection. - */ - private mapHydrateAndFilterRecords( - records: Data.Records, - condition: Contracts.Predicate - ): Data.Collection { - const collection: Data.Collection = [] - - for (const key in records) { - const model = this.hydrate(records[key]) - - condition(model) && collection.push(model) - } - - return collection - } - - /** - * Clears the current state from any data related to current model. - * - * - Everything if not in a inheritance scheme. - * - Only derived instances if applied to a derived entity. - */ - private emptyState(): void { - this.deleteAll() - } - - /** - * Build executable hook collection for the given hook. - */ - private buildHooks(on: string): Contracts.HookableClosure[] { - const hooks = this.getGlobalHookAsArray(on) - - const localHook = this.model[on] as Contracts.HookableClosure | undefined - - localHook && hooks.push(localHook.bind(this.model)) - - return hooks - } - - /** - * Get global hook of the given name as array by stripping id key and keep - * only hook functions. - */ - private getGlobalHookAsArray(on: string): Contracts.HookableClosure[] { - const hooks = this.self().hooks[on] - - return hooks ? hooks.map((h) => h.callback.bind(this)) : [] - } - - /** - * Execute mutation hooks to the given collection. - */ - private executeMutationHooks(on: string, collection: T[]): T[] { - const hooks = this.buildHooks(on) as Contracts.MutationHook[] - - if (hooks.length === 0) { - return collection - } - - return collection.filter((model) => { - return !hooks.some((hook) => hook(model, null, this.entity) === false) - }) - } - - /** - * Execute retrieve hook for the given method. - */ - private executeSelectHook( - on: string, - models: Data.Collection - ): Data.Collection { - const hooks = this.buildHooks(on) as Contracts.SelectHook[] - - return hooks.reduce>((collection, hook) => { - collection = hook(models as any, this.entity) as any - return collection - }, models) - } -} diff --git a/src/query/contracts/GlobalHook.ts b/src/query/contracts/GlobalHook.ts deleted file mode 100644 index 09e65de7..00000000 --- a/src/query/contracts/GlobalHook.ts +++ /dev/null @@ -1,8 +0,0 @@ -import HookableClosure from './HookableClosure' - -export interface GlobalHook { - id: number - callback: HookableClosure -} - -export default GlobalHook diff --git a/src/query/contracts/GlobalHooks.ts b/src/query/contracts/GlobalHooks.ts deleted file mode 100644 index b06890a1..00000000 --- a/src/query/contracts/GlobalHooks.ts +++ /dev/null @@ -1,7 +0,0 @@ -import GlobalHook from './GlobalHook' - -export interface GlobalHooks { - [on: string]: GlobalHook[] -} - -export default GlobalHooks diff --git a/src/query/contracts/HookableClosure.ts b/src/query/contracts/HookableClosure.ts deleted file mode 100644 index 4cf4999c..00000000 --- a/src/query/contracts/HookableClosure.ts +++ /dev/null @@ -1,6 +0,0 @@ -import SelectHook from './SelectHook' -import MutationHook from './MutationHook' - -export type HookableClosure = SelectHook | MutationHook - -export default HookableClosure diff --git a/src/query/contracts/MutationHook.ts b/src/query/contracts/MutationHook.ts deleted file mode 100644 index 2242ca65..00000000 --- a/src/query/contracts/MutationHook.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Model from '../../model/Model' - -export type MutationHook = ( - newModel: Model, - oldModel: Model | null, - entity: string -) => void | false - -export default MutationHook diff --git a/src/query/contracts/Predicate.ts b/src/query/contracts/Predicate.ts deleted file mode 100644 index 864558c9..00000000 --- a/src/query/contracts/Predicate.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Model from '../../model/Model' - -export type Predicate = (model: M) => boolean - -export default Predicate diff --git a/src/query/contracts/RelationshipConstraint.ts b/src/query/contracts/RelationshipConstraint.ts deleted file mode 100644 index 24d3f01e..00000000 --- a/src/query/contracts/RelationshipConstraint.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Query from '../Query' - -export type RelationshipConstraint = (query: Query) => void - -export default RelationshipConstraint diff --git a/src/query/contracts/SelectHook.ts b/src/query/contracts/SelectHook.ts deleted file mode 100644 index 0f9c946e..00000000 --- a/src/query/contracts/SelectHook.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Collection from '../../data/Collection' - -export type SelectHook = ( - models: T, - entity: string -) => T - -export default SelectHook diff --git a/src/query/contracts/index.ts b/src/query/contracts/index.ts deleted file mode 100644 index c07d32cc..00000000 --- a/src/query/contracts/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Predicate from './Predicate' -import RelationshipConstraint from './RelationshipConstraint' -import GlobalHook from './GlobalHook' -import GlobalHooks from './GlobalHooks' -import SelectHook from './SelectHook' -import MutationHook from './MutationHook' -import HookableClosure from './HookableClosure' - -export { - Predicate, - RelationshipConstraint, - GlobalHook, - GlobalHooks, - SelectHook, - MutationHook, - HookableClosure -} diff --git a/src/query/filters/Filter.ts b/src/query/filters/Filter.ts deleted file mode 100644 index 7c9e40c5..00000000 --- a/src/query/filters/Filter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Model from '../../model/Model' -import Collection from '../../data/Collection' -import Query from '../Query' -import WhereFilter from './WhereFilter' -import OrderByFilter from './OrderByFilter' -import LimitFilter from './LimitFilter' - -export default class Filter { - /** - * Filter the given data by registered where clause. - */ - static where( - query: Query, - records: Collection - ): Collection { - return WhereFilter.filter(query, records) - } - - /** - * Sort the given data by registered orders. - */ - static orderBy( - query: Query, - records: Collection - ): Collection { - return OrderByFilter.filter(query, records) - } - - /** - * Limit the given records by the lmilt and offset. - */ - static limit( - query: Query, - records: Collection - ): Collection { - return LimitFilter.filter(query, records) - } -} diff --git a/src/query/filters/LimitFilter.ts b/src/query/filters/LimitFilter.ts deleted file mode 100644 index ecafe055..00000000 --- a/src/query/filters/LimitFilter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Model from '../../model/Model' -import Collection from '../../data/Collection' -import Query from '../Query' - -export default class LimitFilter { - /** - * Limit the given records by the lmilt and offset. - */ - static filter( - query: Query, - records: Collection - ): Collection { - return records.slice( - query.offsetNumber, - query.offsetNumber + query.limitNumber - ) - } -} diff --git a/src/query/filters/OrderByFilter.ts b/src/query/filters/OrderByFilter.ts deleted file mode 100644 index e3a52675..00000000 --- a/src/query/filters/OrderByFilter.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Utils from '../../support/Utils' -import Model from '../../model/Model' -import Collection from '../../data/Collection' -import Query from '../Query' - -export default class OrderByFilter { - /** - * Sort the given data by registered orders. - */ - static filter( - query: Query, - records: Collection - ): Collection { - if (query.orders.length === 0) { - return records - } - - const keys = query.orders.map((order) => order.key) - const directions = query.orders.map((order) => order.direction) - - return Utils.orderBy(records, keys, directions) - } -} diff --git a/src/query/filters/WhereFilter.ts b/src/query/filters/WhereFilter.ts deleted file mode 100644 index f2e7eeb4..00000000 --- a/src/query/filters/WhereFilter.ts +++ /dev/null @@ -1,98 +0,0 @@ -import Model from '../../model/Model' -import Utils from '../../support/Utils' -import Instance from '../../data/Instance' -import Collection from '../../data/Collection' -import * as Options from '../options' -import Query from '../Query' - -export default class WhereFilter { - /** - * Filter the given data by registered where clause. - */ - static filter( - query: Query, - records: Collection - ): Collection { - if (query.wheres.length === 0) { - return records - } - - return records.filter((record) => this.check(query, record)) - } - - /** - * Checks if given Record matches the registered where clause. - */ - static check(query: Query, record: Instance): boolean { - const whereTypes = Utils.groupBy(query.wheres, (where) => where.boolean) - const comparator = this.getComparator(query, record) - - let results: boolean[] = [] - - whereTypes.and && results.push(whereTypes.and.every(comparator)) - - whereTypes.or && results.push(whereTypes.or.some(comparator)) - - return results.indexOf(true) !== -1 - } - - /** - * Get comparator for the where clause. - */ - static getComparator( - query: Query, - record: Instance - ): (where: Options.Where) => boolean { - return (where: Options.Where): boolean => { - // Function with Record and Query as argument. - if (typeof where.field === 'function') { - const newQuery = new Query(query.store, query.entity) - const result = this.executeWhereClosure(newQuery, record, where.field) - - if (typeof result === 'boolean') { - return result - } - - // If closure returns undefined, we need to execute the local query. - const matchingRecords = newQuery.get() - - // And check if current record is part of the result. - return !Utils.isEmpty( - matchingRecords.filter((rec: Instance): boolean => { - return rec['$id'] === record['$id'] - }) - ) - } - - // Function with Record value as argument. - if (typeof where.value === 'function') { - return where.value(record[where.field]) - } - - // Check if field value is in given where Array. - if (Utils.isArray(where.value)) { - return where.value.indexOf(record[where.field]) !== -1 - } - - // Simple equal check. - return record[where.field] === where.value - } - } - - /** - * Execute where closure. - */ - static executeWhereClosure( - query: Query, - record: Instance, - closure: Options.WherePrimaryClosure - ): boolean | void { - if (closure.length !== 3) { - return closure(record, query) - } - - const model = new query.model(record) - - return closure(record, query, model) - } -} diff --git a/src/query/loaders/Loader.ts b/src/query/loaders/Loader.ts deleted file mode 100644 index c16cae1f..00000000 --- a/src/query/loaders/Loader.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { isArray } from '../../support/Utils' -import Collection from '../../data/Collection' -import Relation from '../../attributes/relations/Relation' -import Constraint from '../contracts/RelationshipConstraint' -import Query from '../Query' - -export default class Loader { - /** - * Set the relationships that should be eager loaded with the query. - */ - static with( - query: Query, - name: string | string[], - constraint: Constraint | null - ): void { - // If the name of the relation is `*`, we'll load all relationships. - if (name === '*') { - this.withAll(query, constraint) - - return - } - - // If we passed an array, we dispatch the bits to with queries. - if (isArray(name)) { - name.forEach((relationName) => this.with(query, relationName, constraint)) - - return - } - - // Else parse relations and set appropriate constraints. - this.parseWithRelations(query, name.split('.'), constraint) - } - - /** - * Set all relationships to be eager loaded with the query. - */ - static withAll(query: Query, constraint: Constraint | null): void { - const fields = query.model.getFields() - - for (const field in fields) { - fields[field] instanceof Relation && this.with(query, field, constraint) - } - } - - /** - * Set relationships to be recursively eager loaded with the query. - */ - static withAllRecursive(query: Query, depth: number): void { - this.withAll(query, (relatedQuery) => { - depth > 0 && relatedQuery.withAllRecursive(depth - 1) - }) - } - - /** - * Set eager load relation and constraint. - */ - private static setEagerLoad( - query: Query, - name: string, - constraint: Constraint | null = null - ): void { - if (!query.load[name]) { - query.load[name] = [] - } - - constraint && query.load[name].push(constraint) - } - - /** - * Parse a list of relations into individuals. - */ - private static parseWithRelations( - query: Query, - relations: string[], - constraint: Constraint | null - ): void { - // First we'll get the very first relationship from teh whole relations. - const relation = relations[0] - - // If the first relation has "or" syntax which is `|` for example - // `posts|videos`, set each of them as separate eager load. - relation.split('|').forEach((name) => { - // If there's only one relationship in relations array, that means - // there's no nested relationship. So we'll set the given - // constraint to the relationship loading. - if (relations.length === 1) { - this.setEagerLoad(query, name, constraint) - - return - } - - // Else we'll skip adding constraint because the constraint has to be - // applied to the nested relationship. We'll let `addNestedWiths` - // method to handle that later. - this.setEagerLoad(query, name) - }) - - // If the given relations only contains a single name, which means it - // doesn't have any nested relations such as `posts.comments`, we - // don't need go farther so return here. - if (relations.length === 1) { - return - } - - // Finally, we shift the first relation from the array and handle lest - // of relations as a nested relation. - relations.shift() - - this.addNestedWiths(query, relation, relations, constraint) - } - - /** - * Parse the nested relationships in a relation. - */ - private static addNestedWiths( - query: Query, - name: string, - children: string[], - constraint: Constraint | null - ): void { - this.setEagerLoad(query, name, (nestedQuery) => { - nestedQuery.with(children.join('.'), constraint) - }) - } - - /** - * Eager load the relationships for the given collection. - */ - static eagerLoadRelations(query: Query, collection: Collection): void { - const fields = query.model.getFields() - - for (const name in query.load) { - const constraints = query.load[name] - let relation = fields[name] - - if (relation instanceof Relation) { - relation.load(query, collection, name, constraints) - continue - } - - // If no relation was found on the query, it might be run on the - // base entity of a hierarchy. In this case, we try looking up - // the relation on the derived entities - if (query.model.hasTypes()) { - const candidateRelation = query.model.findRelationInSubTypes(name) - if (candidateRelation !== null) { - candidateRelation.load(query, collection, name, constraints) - } - } - } - } -} diff --git a/src/query/options/Constraint.ts b/src/query/options/Constraint.ts deleted file mode 100644 index c8e16fdd..00000000 --- a/src/query/options/Constraint.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Query from '../Query' - -export type Constraint = (query: Query) => boolean | null | void - -export default Constraint diff --git a/src/query/options/Has.ts b/src/query/options/Has.ts deleted file mode 100644 index e9e0459b..00000000 --- a/src/query/options/Has.ts +++ /dev/null @@ -1,11 +0,0 @@ -import HasConstraint from './HasConstraint' - -export interface Has { - relation: string - type: string - operator: string - count: number - constraint: HasConstraint | null -} - -export default Has diff --git a/src/query/options/HasConstraint.ts b/src/query/options/HasConstraint.ts deleted file mode 100644 index 8ec64857..00000000 --- a/src/query/options/HasConstraint.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Query from '../Query' - -export type Constraint = (query: Query) => boolean | void - -export default Constraint diff --git a/src/query/options/Load.ts b/src/query/options/Load.ts deleted file mode 100644 index 21af0a1e..00000000 --- a/src/query/options/Load.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Constraint from './Constraint' - -export interface Load { - [relation: string]: Constraint[] -} - -export default Load diff --git a/src/query/options/OrderDirection.ts b/src/query/options/OrderDirection.ts deleted file mode 100644 index 28d02db9..00000000 --- a/src/query/options/OrderDirection.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type OrderDirection = 'asc' | 'desc' - -export default OrderDirection diff --git a/src/query/options/OrderKey.ts b/src/query/options/OrderKey.ts deleted file mode 100644 index b66cd566..00000000 --- a/src/query/options/OrderKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Model from '../../model/Model' - -export type OrderKey = string | ((record: T) => any) - -export default OrderKey diff --git a/src/query/options/Orders.ts b/src/query/options/Orders.ts deleted file mode 100644 index f309948b..00000000 --- a/src/query/options/Orders.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Model from '../../model/Model' -import OrderKey from './OrderKey' -import OrderDirection from './OrderDirection' - -export interface Orders { - key: OrderKey - direction: OrderDirection -} - -export default Orders diff --git a/src/query/options/PersistMethods.ts b/src/query/options/PersistMethods.ts deleted file mode 100644 index 39b241f0..00000000 --- a/src/query/options/PersistMethods.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type PersistMethods = 'create' | 'insert' | 'update' | 'insertOrUpdate' - -export default PersistMethods diff --git a/src/query/options/PersistOptions.ts b/src/query/options/PersistOptions.ts deleted file mode 100644 index 239c835f..00000000 --- a/src/query/options/PersistOptions.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface PersistOptions { - create?: string[] - insert?: string[] - update?: string[] - insertOrUpdate?: string[] -} - -export default PersistOptions diff --git a/src/query/options/PrimaryKey.ts b/src/query/options/PrimaryKey.ts deleted file mode 100644 index a094c007..00000000 --- a/src/query/options/PrimaryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type PrimaryKey = string | number | (string | number)[] - -export default PrimaryKey diff --git a/src/query/options/Where.ts b/src/query/options/Where.ts deleted file mode 100644 index 07c1ff2b..00000000 --- a/src/query/options/Where.ts +++ /dev/null @@ -1,11 +0,0 @@ -import WhereBoolean from './WhereBoolean' -import WherePrimaryClosure from './WherePrimaryClosure' -import WhereSecondaryClosure from './WhereSecondaryClosure' - -export interface Where { - field: string | number | WherePrimaryClosure - value: string | number | WhereSecondaryClosure - boolean: WhereBoolean -} - -export default Where diff --git a/src/query/options/WhereBoolean.ts b/src/query/options/WhereBoolean.ts deleted file mode 100644 index b1a7b345..00000000 --- a/src/query/options/WhereBoolean.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type WhereBoolean = 'and' | 'or' - -export default WhereBoolean diff --git a/src/query/options/WherePrimaryClosure.ts b/src/query/options/WherePrimaryClosure.ts deleted file mode 100644 index d4a1d655..00000000 --- a/src/query/options/WherePrimaryClosure.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Record from '../../data/Record' -import Model from '../../model/Model' -import Query from '../Query' - -export type WherePrimaryClosure = ( - record: Record, - query: Query, - model?: Model -) => boolean | void - -export default WherePrimaryClosure diff --git a/src/query/options/WhereSecondaryClosure.ts b/src/query/options/WhereSecondaryClosure.ts deleted file mode 100644 index 5c68d893..00000000 --- a/src/query/options/WhereSecondaryClosure.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type WhereSecondaryClosure = (value: any) => boolean - -export default WhereSecondaryClosure diff --git a/src/query/options/index.ts b/src/query/options/index.ts deleted file mode 100644 index 78821c86..00000000 --- a/src/query/options/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Where from './Where' -import WhereBoolean from './WhereBoolean' -import WherePrimaryClosure from './WherePrimaryClosure' -import WhereSecondaryClosure from './WhereSecondaryClosure' -import Has from './Has' -import HasConstraint from './HasConstraint' -import Orders from './Orders' -import OrderKey from './OrderKey' -import OrderDirection from './OrderDirection' -import Constraint from './Constraint' -import Load from './Load' -import PersistMethods from './PersistMethods' -import PersistOptions from './PersistOptions' -import PrimaryKey from './PrimaryKey' - -export { - Where, - WhereBoolean, - WherePrimaryClosure, - WhereSecondaryClosure, - Has, - HasConstraint, - Orders, - OrderKey, - OrderDirection, - Constraint, - Load, - PersistMethods, - PersistOptions, - PrimaryKey -} diff --git a/src/query/processors/Attacher.ts b/src/query/processors/Attacher.ts deleted file mode 100644 index 5d9972c4..00000000 --- a/src/query/processors/Attacher.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Utils from '../../support/Utils' -import NormalizedData from '../../data/NormalizedData' -import Relation from '../../attributes/relations/Relation' -import Query from '../../query/Query' - -export default class Attacher { - /** - * Attach missing relational key to the records. - */ - static process(query: Query, data: NormalizedData): NormalizedData { - Utils.forOwn(data, (entity, name) => { - const fields = query.getModel(name).fields() - - Utils.forOwn(entity, (record) => { - Utils.forOwn(record, (value, key) => { - const field = fields[key] - - if (field instanceof Relation) { - value !== null && field.attach(value, record, data) - } - }) - }) - }) - - return data - } -} diff --git a/src/query/processors/Normalizer.ts b/src/query/processors/Normalizer.ts deleted file mode 100644 index 3bc032ca..00000000 --- a/src/query/processors/Normalizer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { normalize } from 'normalizr' -import Utils from '../../support/Utils' -import Record from '../../data/Record' -import NormalizedData from '../../data/NormalizedData' -import Query from '../../query/Query' - -export default class Normalizer { - /** - * Normalize the record. - */ - static process(query: Query, record: Record | Record[]): NormalizedData { - if (Utils.isEmpty(record)) { - return {} - } - - const entity = query.database.schemas[query.model.entity] - - const schema = Utils.isArray(record) ? [entity] : entity - - return normalize(record, schema).entities as NormalizedData - } -} diff --git a/src/query/processors/PivotCreator.ts b/src/query/processors/PivotCreator.ts deleted file mode 100644 index 21b0231b..00000000 --- a/src/query/processors/PivotCreator.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Utils from '../../support/Utils' -import NormalizedData from '../../data/NormalizedData' -import Query from '../../query/Query' - -export default class PivotCreator { - /** - * Create an intermediate entity if the data contains any entities that - * require it for example `belongsTo` or `morphMany`. - */ - static process(query: Query, data: NormalizedData): NormalizedData { - Object.keys(data).forEach((entity) => { - const model = query.getModel(entity) - - if (model.hasPivotFields()) { - Utils.forOwn(model.pivotFields(), (field: any) => { - Utils.forOwn(field, (attr, key) => { - attr.createPivots(model, data, key) - }) - }) - } - }) - - return data - } -} diff --git a/src/query/processors/Processor.ts b/src/query/processors/Processor.ts deleted file mode 100644 index fd2785f4..00000000 --- a/src/query/processors/Processor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Record from '../../data/Record' -import NormalizedData from '../../data/NormalizedData' -import Query from '../Query' -import Normalizer from './Normalizer' -import PivotCreator from './PivotCreator' -import Attacher from './Attacher' - -export default class Processor { - /** - * Normalize the given data. - */ - static normalize(query: Query, record: Record | Record[]): NormalizedData { - // First, let's normalize the data. - let data = Normalizer.process(query, record) - - // Then, attach any missing foreign keys. For example, if a User has many - // Post nested but without foreign key such as `user_id`, we can attach - // the `user_id` value to the Post entities. - data = Attacher.process(query, data) - // Now we'll create any missing pivot entities for relationships such as - // `belongsTo` or `morphMany`. - data = PivotCreator.process(query, data) - - // And we'll return the result as a normalized data. - return data - } -} diff --git a/src/query/rollcallers/Rollcaller.ts b/src/query/rollcallers/Rollcaller.ts deleted file mode 100644 index 77838bd0..00000000 --- a/src/query/rollcallers/Rollcaller.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { isArray } from '../../support/Utils' -import Item from '../../data/Item' -import Collection from '../../data/Collection' -import Model from '../../model/Model' -import Predicate from '../contracts/Predicate' -import Has from '../options/Has' -import HasConstraint from '../options/HasConstraint' -import Query from '../Query' - -export default class Rollcaller { - /** - * Set where constraint based on relationship existence. - */ - static has( - query: Query, - relation: string, - operator?: string | number, - count?: number - ): void { - this.setHas(query, relation, 'exists', operator, count) - } - - /** - * Set where constraint based on relationship absence. - */ - static hasNot( - query: Query, - relation: string, - operator?: string | number, - count?: number - ): void { - this.setHas(query, relation, 'doesntExist', operator, count) - } - - /** - * Add where has condition. - */ - static whereHas( - query: Query, - relation: string, - constraint: HasConstraint - ): void { - this.setHas(query, relation, 'exists', undefined, undefined, constraint) - } - - /** - * Add where has not condition. - */ - static whereHasNot( - query: Query, - relation: string, - constraint: HasConstraint - ): void { - this.setHas( - query, - relation, - 'doesntExist', - undefined, - undefined, - constraint - ) - } - - /** - * Set `has` condition. - */ - private static setHas( - query: Query, - relation: string, - type: string, - operator: string | number = '>=', - count: number = 1, - constraint: HasConstraint | null = null - ): void { - if (typeof operator === 'number') { - query.have.push({ - relation, - type, - operator: '>=', - count: operator, - constraint - }) - - return - } - - query.have.push({ relation, type, operator, count, constraint }) - } - - /** - * Convert `has` conditions to where clause. It will check any relationship - * existence, or absence for the records then set ids of the records that - * matched the condition to `where` clause. - * - * This way, when the query gets executed, only those records that matched - * the `has` condition get retrieved. In the future, once relationship index - * mapping is implemented, we can simply do all checks inside the where - * filter since we can treat `has` condition as usual `where` condition. - * - * For now, since we must fetch any relationship by eager loading them, due - * to performance concern, we'll apply `has` conditions this way to gain - * maximum performance. - */ - static applyConstraints(query: Query): void { - if (query.have.length === 0) { - return - } - - const newQuery = query.newQuery() - - this.addHasWhereConstraints(query, newQuery) - - this.addHasConstraints(query, newQuery.get()) - } - - /** - * Add has constraints to the given query. It's going to set all relationship - * as `with` alongside with its closure constraints. - */ - private static addHasWhereConstraints(query: Query, newQuery: Query): void { - query.have.forEach((constraint) => { - newQuery.with(constraint.relation, constraint.constraint) - }) - } - - /** - * Add has constraints as where clause. - */ - private static addHasConstraints(query: Query, collection: Collection): void { - const comparators = this.getComparators(query) - - const ids: string[] = [] - - collection.forEach((model) => { - if (comparators.every((comparator) => comparator(model))) { - ids.push(model.$self().getIdFromRecord(model) as string) - } - }) - - query.whereIdIn(ids) - } - - /** - * Get comparators for the has clause. - */ - private static getComparators(query: Query): Predicate[] { - return query.have.map((constraint) => this.getComparator(constraint)) - } - - /** - * Get a comparator for the has clause. - */ - private static getComparator(constraint: Has): Predicate { - const compare = this.getCountComparator(constraint.operator) - - return (model: Model): boolean => { - const count = this.getRelationshipCount(model[constraint.relation]) - - const result = compare(count, constraint.count) - - return constraint.type === 'exists' ? result : !result - } - } - - /** - * Get count of the relationship. - */ - private static getRelationshipCount(relation: Collection | Item): number { - if (isArray(relation)) { - return relation.length - } - - return relation ? 1 : 0 - } - - /** - * Get comparator function for the `has` clause. - */ - private static getCountComparator( - operator: string - ): (x: number, y: number) => boolean { - switch (operator) { - case '=': - return (x: number, y: number): boolean => x === y - - case '>': - return (x: number, y: number): boolean => x > y - - case '>=': - return (x: number, y: number): boolean => x >= y - - case '<': - return (x: number, y: number): boolean => x > 0 && x < y - - case '<=': - return (x: number, y: number): boolean => x > 0 && x <= y - - default: - return (x: number, y: number): boolean => x === y - } - } -} diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts deleted file mode 100644 index 9f81ab90..00000000 --- a/src/repository/Repository.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Store } from 'vuex' -import { ConstructorOf as BaseConstructorOf } from '../types' -import Database from '../database/Database' -import { Record, Item, Collection, Collections } from '../data' -import Model from '../model/Model' -import * as Payloads from '../modules/payloads/Actions' -import PayloadBuilder from '../modules/support/PayloadBuilder' -import Query from '../query/Query' -import Predicate from '../query/contracts/Predicate' -import { PersistOptions } from '../query/options' -import { InsertObject, normalizeInsertPayload } from './support/Payloads' - -interface ConstructorOf extends BaseConstructorOf { - entity: string -} - -export default 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: boolean = true - - /** - * The store instance for the repository. - */ - store: Store - - /** - * The model for the repository. - */ - model!: ConstructorOf - - /** - * Create a new repository instance. - */ - constructor(store: Store, model?: ConstructorOf) { - this.store = store - - if (model) { - this.model = model - } - } - - /** - * Get the database instance from the store instance. - */ - database(): Database { - return this.store.$database - } - - /** - * Create a namespaced method name for Vuex Module from the given - * method name. - */ - namespace(method: string): string { - return `${this.database().namespace}/${this.model.entity}/${method}` - } - - /** - * Call Vuex Getters. - */ - getters(method: string): any { - return this.store.getters[this.namespace(method)] - } - - /** - * Dispatch Vuex Action. - */ - dispatch(method: string, payload?: any): Promise { - return this.store.dispatch(this.namespace(method), payload) - } - - /** - * Create a new model instance. - */ - make(record?: Record): M { - return new this.model(record) - } - - /** - * Get all records. - */ - all(): Collection { - return this.getters('all')() - } - - /** - * Find a record. - */ - find(id: string | number | (number | string)[]): Item { - return this.getters('find')(id) - } - - /** - * Get the record of the given array of ids. - */ - findIn(idList: (number | string | (number | string)[])[]): Collection { - return this.getters('findIn')(idList) - } - - /** - * Get query instance. - */ - query(): Query { - return new Query(this.store, this.model.entity) - } - - /** - * Check wether the associated database contains data. - */ - exists(): boolean { - return this.query().exists() - } - - /** - * Create new data with all fields filled by default values. - */ - new(): Promise { - return this.query().new() - } - - /** - * Save given data to the store by replacing all existing records in the - * store. If you want to save data without replacing existing records, - * use the `insert` method instead. - */ - create( - data: Record | Record[] | InsertObject, - options?: PersistOptions - ): Promise { - const { data: d, options: o } = normalizeInsertPayload(data, options) - - return this.query().create(d, o) - } - - /** - * Insert records. - */ - insert( - data: Record | Record[] | InsertObject, - options?: PersistOptions - ): Promise { - const { data: d, options: o } = normalizeInsertPayload(data, options) - - return this.query().insert(d, o) - } - - /** - * Update records. - */ - update( - payload: Payloads.Update, - options?: PersistOptions - ): Promise { - return this.dispatch('update', PayloadBuilder.normalize(payload, options)) - } - - /** - * Insert or update records. - */ - insertOrUpdate( - payload: Payloads.InsertOrUpdate, - options?: PersistOptions - ): Promise { - return this.dispatch( - 'insertOrUpdate', - PayloadBuilder.normalize(payload, options) - ) - } - - /** - * Delete records that matches the given condition. - */ - delete(id: string | number | (number | string)[]): Promise> - delete(condition: Predicate): Promise> - delete(payload: any): any { - return this.dispatch('delete', payload) - } - - /** - * Delete all records from the store. - */ - deleteAll(): Promise> { - return this.dispatch('deleteAll') - } -} diff --git a/src/repository/support/Payloads.ts b/src/repository/support/Payloads.ts deleted file mode 100644 index 69332c76..00000000 --- a/src/repository/support/Payloads.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { isArray } from '../../support/Utils' -import { Record } from '../../data' -import { PersistOptions } from '../../query/options' - -export interface InsertObject extends PersistOptions { - data: Record | Record[] -} - -export interface InsertPayload { - data: Record | Record[] - options?: PersistOptions -} - -/** - * Normalize the payload arguments for persist methods. It's required for - * backward compatibility for the new persist method argument structure. - * It may be deprecated and be removed in the future. - */ -export function normalizeInsertPayload( - data: Record | Record[] | InsertObject, - options?: PersistOptions -): InsertPayload { - // If the data is an array, then the data should be an array of data. - // Therefore the whole payload should be passed directly as `data`. - if (isArray(data)) { - return { data, options } - } - - // If the data doesn't have a `data` property, it can be assumed that the - // user has passed an object as the payload. Therefore the entire payload - // should be declared as `data`. - if (data.data === undefined) { - return { data, options } - } - - // If the data contains `data` property, we should extract it and pass that - // property as `data`. - return { data: data.data, options: data } -} diff --git a/src/schema/IdAttribute.ts b/src/schema/IdAttribute.ts deleted file mode 100644 index 79d80467..00000000 --- a/src/schema/IdAttribute.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { schema } from 'normalizr' -import { isArray } from '../support/Utils' -import Uid from '../support/Uid' -import Record from '../data/Record' -import UidAttribute from '../attributes/types/Uid' -import Model from '../model/Model' - -export default class IdAttribute { - /** - * Creates a closure that generates the required id's for an entity. - */ - static create(model: typeof Model): schema.StrategyFunction { - return (value: any, _parentValue: any, _key: string): string => { - this.generateIds(value, model) - - const indexId = this.generateIndexId(value, model) - - return indexId - } - } - - /** - * Generate a field that is defined as primary keys. For keys with a proper - * value set, it will do nothing. If a key is missing, it will generate - * UID for it. - */ - private static generateIds(record: Record, model: typeof Model): void { - const keys = isArray(model.primaryKey) - ? model.primaryKey - : [model.primaryKey] - - keys.forEach((k) => { - if (record[k] !== undefined && record[k] !== null) { - return - } - - const attr = model.getFields()[k] - - record[k] = attr instanceof UidAttribute ? attr.make() : Uid.make() - }) - } - - /** - * Generate index id field (which is `$id`) and attach to the given record. - */ - private static generateIndexId(record: Record, model: typeof Model): string { - record.$id = model.getIndexIdFromRecord(record) - - return record.$id - } -} diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts deleted file mode 100644 index 1735a66b..00000000 --- a/src/schema/Schema.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { schema as Normalizr, Schema as NormalizrSchema } from 'normalizr' -import Model from '../model/Model' -import Relation from '../attributes/relations/Relation' -import Schemas from './Schemas' -import IdAttribute from './IdAttribute' - -export default class Schema { - /** - * List of generated schemas. - */ - schemas: Schemas = {} - - /** - * The model class. - */ - model: typeof Model - - /** - * Create a new schema instance. - */ - constructor(model: typeof Model) { - this.model = model - - const models = model.database().models() - - Object.keys(models).forEach((name) => { - this.one(models[name]) - }) - } - - /** - * Create a schema for the given model. - */ - static create(model: typeof Model): Normalizr.Entity { - return new this(model).one() - } - - /** - * Create a single schema for the given model. - */ - one(model?: typeof Model): Normalizr.Entity { - model = model || this.model - - if (this.schemas[model.entity]) { - return this.schemas[model.entity] - } - - const schema = new Normalizr.Entity( - model.entity, - {}, - { - idAttribute: IdAttribute.create(model) - } - ) - - this.schemas[model.entity] = schema - - const definition = this.definition(model) - - schema.define(definition) - - return schema - } - - /** - * Create an array schema for the given model. - */ - many(model: typeof Model): Normalizr.Array { - return new Normalizr.Array(this.one(model)) - } - - /** - * Create an union schema for the given model. - */ - union(callback: Normalizr.SchemaFunction): Normalizr.Union { - return new Normalizr.Union(this.schemas, callback) - } - - /** - * Create a dfinition for the given model. - */ - definition(model: typeof Model): NormalizrSchema { - const fields = model.getFields() - - return Object.keys(fields).reduce((definition, key) => { - const field = fields[key] - - if (field instanceof Relation) { - definition[key] = field.define(this) - } - - return definition - }, {}) - } -} diff --git a/src/schema/Schemas.ts b/src/schema/Schemas.ts deleted file mode 100644 index 1bf738e8..00000000 --- a/src/schema/Schemas.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { schema as NormalizrSchema } from 'normalizr' - -export interface Schemas { - [entity: string]: NormalizrSchema.Entity -} - -export default Schemas diff --git a/src/store/index.ts b/src/store/index.ts deleted file mode 100644 index 552b056f..00000000 --- a/src/store/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Store, Plugin } from 'vuex' -import Container from '../container/Container' -import Database from '../database/Database' -import HackedDatabase from '../database/HackedDatabase' -import Repository from '../repository/Repository' - -export interface Options { - namespace?: string -} - -/** - * Install Vuex ORM database to the store. - */ -export function install( - database: Database, - options: Options = {} -): Plugin { - const namespace = options.namespace || 'entities' - - return (store) => { - mixin(store, database, namespace) - } -} - -/** - * Mixin Vuex ORM feature to the store. - */ -function mixin(store: Store, database: Database, namespace: string): void { - registerDatabase(store, database) - mixinRepoFunction(store) - mixinDbFunction(store) - registerToContainer(store) - startDatabase(store, namespace) -} - -/** - * Register the database to the store. - */ -function registerDatabase(store: Store, database: Database): void { - store.$database = database -} - -/** - * Mixin repo function to the store. - */ -function mixinRepoFunction(store: Store): void { - store.$repo = function(modelOrRepository: any): any { - const repository = modelOrRepository._isRepository - ? new modelOrRepository(this) - : new Repository(this, modelOrRepository) - - if (!repository.model) { - throw new Error( - '[Vuex ORM] The repository was instantiated without a model being ' + - 'set. It happens when you forgot to register the model to the ' + - 'custom repository. Please check if you have correctly set ' + - '`model` property at the repository class.' - ) - } - - return repository - } -} - -/** - * Mixin db function to the store. - */ -function mixinDbFunction(store: Store): void { - store.$db = function(): HackedDatabase { - /* istanbul ignore next */ - if (__DEV__) { - console.warn( - '[Vuex ORM] `store.$db()` method is deprecated. Please use ' + - '`store.$repo(Model)` method instead.' - ) - } - - return new HackedDatabase(this.$database) - } -} - -/** - * Register the store to the container. - */ -function registerToContainer(store: Store): void { - Container.register(store) -} - -/** - * Start the database. - */ -function startDatabase(store: Store, namespace: string): void { - store.$database.start(store, namespace) -} diff --git a/src/support/Uid.ts b/src/support/Uid.ts deleted file mode 100644 index cb9b773e..00000000 --- a/src/support/Uid.ts +++ /dev/null @@ -1,27 +0,0 @@ -export default class Uid { - /** - * Count to create a unique id. - */ - private static count: number = 0 - - /** - * Prefix string to be used for the id. - */ - private static prefix: string = '$uid' - - /** - * Generate an UUID. - */ - static make(): string { - this.count++ - - return `${this.prefix}${this.count}` - } - - /** - * Reset the count to 0. - */ - static reset(): void { - this.count = 0 - } -} diff --git a/src/support/Utils.ts b/src/support/Utils.ts deleted file mode 100644 index f12c7f37..00000000 --- a/src/support/Utils.ts +++ /dev/null @@ -1,285 +0,0 @@ -export interface Dictionary { - [key: string]: T -} - -export type Predicate = (value: T, key: string) => boolean - -export type ObjectIteratee = ( - value: T[keyof T], - key: string, - object: T -) => TResult - -interface SortableArray { - criteria: any[] - index: number - value: T -} - -/** - * Check if the given value is the type of array. - */ -export function isArray(value: any): value is any[] { - return Array.isArray(value) -} - -/** - * Gets the size of collection by returning its length for array-like values - * or the number of own enumerable string keyed properties for objects. - */ -export function size(collection: any[] | object): number { - return isArray(collection) - ? collection.length - : Object.keys(collection).length -} - -/** - * Check if the given array or object is empty. - */ -export function isEmpty(collection: any[] | object): boolean { - return size(collection) === 0 -} - -/** - * Iterates over own enumerable string keyed properties of an object and - * invokes `iteratee` for each property. - */ -export function forOwn( - object: T, - iteratee: ObjectIteratee -): void { - Object.keys(object).forEach((key) => iteratee(object[key], key, object)) -} - -/** - * Creates an array of values by running each element in collection thru - * iteratee. The iteratee is invoked with three arguments: - * (value, key, collection). - */ -export function map( - object: T, - iteratee: ObjectIteratee -): TResult[] { - const result: TResult[] = [] - - for (const key in object) { - result.push(iteratee(object[key], key, object)) - } - - return result -} - -/** - * Creates an object with the same keys as object and values generated by - * running each own enumerable string keyed property of object thru - * iteratee. The iteratee is invoked with three arguments: - * (value, key, object). - */ -export function mapValues( - object: T, - iteratee: ObjectIteratee -): Dictionary { - const newObject = Object.assign({}, object) - - return Object.keys(object).reduce((records, key) => { - records[key] = iteratee(object[key], key, object) - return records - }, newObject) -} - -/** - * Creates an object composed of keys generated from the results of running - * each element of collection by the given key. - */ -export function keyBy( - collection: T[], - key: string -): Record { - const o: Record = {} - - collection.forEach((item) => { - o[item[key]] = item - }) - - return o -} - -/** - * Creates an array of elements, sorted in specified order by the results - * of running each element in a collection thru each iteratee. - */ -export function orderBy( - collection: T[], - iteratees: (((record: T) => any) | string)[], - directions: string[] -): T[] { - let index = -1 - - const result = collection.map((value) => { - const criteria = iteratees.map((iteratee) => { - return typeof iteratee === 'function' ? iteratee(value) : value[iteratee] - }) - - return { criteria, index: ++index, value } - }) - - return baseSortBy(result, (object, other) => { - return compareMultiple(object, other, directions) - }) -} - -/** - * Creates an array of elements, sorted in ascending order by the results of - * running each element in a collection thru each iteratee. This method - * performs a stable sort, that is, it preserves the original sort order - * of equal elements. - */ -function baseSortBy( - array: SortableArray[], - comparer: (a: SortableArray, B: SortableArray) => number -): T[] { - let length = array.length - - array.sort(comparer) - - const newArray: T[] = [] - while (length--) { - newArray[length] = array[length].value - } - return newArray -} - -/** - * Used by `orderBy` to compare multiple properties of a value to another - * and stable sort them. - * - * If `orders` is unspecified, all values are sorted in ascending order. - * Otherwise, specify an order of "desc" for descending or "asc" for - * ascending sort order of corresponding values. - */ -function compareMultiple(object: any, other: any, orders: string[]): number { - let index = -1 - - const objCriteria = object.criteria - const othCriteria = other.criteria - const length = objCriteria.length - const ordersLength = orders.length - - while (++index < length) { - const result = compareAscending(objCriteria[index], othCriteria[index]) - - if (result) { - if (index >= ordersLength) { - return result - } - - const order = orders[index] - - return result * (order === 'desc' ? -1 : 1) - } - } - - return object.index - other.index -} - -/** - * Compares values to sort them in ascending order. - */ -function compareAscending(value: any, other: any): number { - if (value !== other) { - const valIsDefined = value !== undefined - const valIsNull = value === null - const valIsReflexive = value === value - - const othIsDefined = other !== undefined - const othIsNull = other === null - const othIsReflexive = other === other - - if (typeof value !== 'number' || typeof other !== 'number') { - value = String(value) - other = String(other) - } - - if ( - (!othIsNull && value > other) || - (valIsNull && othIsDefined && othIsReflexive) || - (!valIsDefined && othIsReflexive) || - !valIsReflexive - ) { - return 1 - } - - if ( - (!valIsNull && value < other) || - (othIsNull && valIsDefined && valIsReflexive) || - (!othIsDefined && valIsReflexive) || - !othIsReflexive - ) { - return -1 - } - } - - return 0 -} - -/** - * Creates an object composed of keys generated from the results of running - * each element of collection thru iteratee. - */ -export function groupBy( - collection: any[], - iteratee: (record: any) => any -): any { - return collection.reduce((records, record) => { - const key = iteratee(record) - - if (records[key] === undefined) { - records[key] = [] - } - - records[key].push(record) - - return records - }, {} as any) -} - -/** - * Deep clone the given target object. - */ -export function cloneDeep(target: T): T { - if (target === null) { - return target - } - - if (isArray(target)) { - const cp = [] as any[] - ;(target as any[]).forEach((v) => cp.push(v)) - - return cp.map((n: any) => cloneDeep(n)) as any - } - - if (typeof target === 'object' && target !== {}) { - const cp = { ...(target as { [key: string]: any }) } as { - [key: string]: any - } - - Object.keys(cp).forEach((k) => (cp[k] = cloneDeep(cp[k]))) - - return cp as T - } - - return target -} - -export default { - isArray, - size, - isEmpty, - forOwn, - map, - mapValues, - keyBy, - orderBy, - groupBy, - cloneDeep -} diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index c5ddf026..00000000 --- a/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type ConstructorOf = { new (...args: any[]): C } diff --git a/src/types/vuex.ts b/src/types/vuex.ts deleted file mode 100644 index 3eb1d98d..00000000 --- a/src/types/vuex.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ConstructorOf } from '../types' -import Database from '../database/Database' -import HackedDatabase from '../database/HackedDatabase' -import Model from '../model/Model' -import Repository from '../repository/Repository' - -declare module 'vuex' { - interface Store { - /** - * The database instance registered to the store. - */ - $database: Database - - /** - * Get a new repository instance for the given model. - */ - $repo>(repository: ConstructorOf): R - $repo(model: ConstructorOf): Repository - - /** - * Get the database attached to the store. It's the old syntax and should - * avoid using it. - * - * It will return a "Hacked" database that capable of finding a copied - * model object. It's an old way of interacting with the database. - * - * TODO: Update the version. - * @deprecated Since v0.XX.X - */ - $db(): HackedDatabase - } -} diff --git a/test/Helpers.ts b/test/Helpers.ts new file mode 100644 index 00000000..c15f26f4 --- /dev/null +++ b/test/Helpers.ts @@ -0,0 +1 @@ +// ... diff --git a/test/feature/aggregates/Aggregates.spec.ts b/test/feature/aggregates/Aggregates.spec.ts deleted file mode 100644 index 98fb11fc..00000000 --- a/test/feature/aggregates/Aggregates.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Aggregates', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - }) - - it('can get count of the data', async () => { - await User.create([ - { id: 1, role: 'admin' }, - { id: 2, role: 'user' }, - { id: 3, role: 'admin' } - ]) - - expect(User.query().count()).toBe(3) - expect( - User.query() - .where('role', 'admin') - .count() - ).toBe(2) - }) - - it('can get max value of the specified field', async () => { - await User.create([ - { id: 8, role: 'admin' }, - { id: 12, role: 'user' }, - { id: 11, role: 'admin' }, - { id: 'A', role: 'admin' } - ]) - - expect(User.query().max('id')).toBe(12) - expect(User.query().max('role')).toBe(0) - expect( - User.query() - .where('role', 'admin') - .max('id') - ).toBe(11) - }) - - it('can get min value of the specified field', async () => { - await User.create([ - { id: 8, role: 'admin' }, - { id: 12, role: 'user' }, - { id: 11, role: 'admin' }, - { id: 'A', role: 'admin' } - ]) - - expect(User.query().min('id')).toBe(8) - expect(User.query().min('role')).toBe(0) - expect( - User.query() - .where('role', 'admin') - .min('id') - ).toBe(8) - }) - - it('can get sum value of the specified field', async () => { - await User.create([ - { id: 8, role: 'admin' }, - { id: 12, role: 'user' }, - { id: 11, role: 'admin' }, - { id: 'A', role: 'admin' } - ]) - - expect(User.query().sum('id')).toBe(31) - expect( - User.query() - .where('role', 'admin') - .sum('id') - ).toBe(19) - }) -}) diff --git a/test/feature/attributes/Boolean.spec.ts b/test/feature/attributes/Boolean.spec.ts deleted file mode 100644 index 6d7b1e83..00000000 --- a/test/feature/attributes/Boolean.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Attributes – Boolean', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Bool(true) - bool!: boolean - - static fields() { - return { - id: this.attr(null), - bool: this.boolean(true) - } - } - } - - it('casts the value to `Boolean` when creating data', async () => { - const store = createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, bool: '' }, - { id: 3, bool: 'string' }, - { id: 4, bool: '0' }, - { id: 5, bool: 0 }, - { id: 6, bool: 1 }, - { id: 7, bool: true }, - { id: 8, bool: null } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, bool: true }, - 2: { $id: '2', id: 2, bool: false }, - 3: { $id: '3', id: 3, bool: true }, - 4: { $id: '4', id: 4, bool: false }, - 5: { $id: '5', id: 5, bool: false }, - 6: { $id: '6', id: 6, bool: true }, - 7: { $id: '7', id: 7, bool: true }, - 8: { $id: '8', id: 8, bool: false } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('casts the value to `Boolean` when retrieving data', async () => { - createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, bool: '' }, - { id: 3, bool: 'string' }, - { id: 4, bool: '0' }, - { id: 5, bool: 0 }, - { id: 6, bool: 1 }, - { id: 7, bool: true }, - { id: 8, bool: null } - ]) - - const users = User.all() - - expect(users[0].bool).toEqual(true) - expect(users[1].bool).toEqual(false) - expect(users[2].bool).toEqual(true) - expect(users[3].bool).toEqual(false) - expect(users[4].bool).toEqual(false) - expect(users[5].bool).toEqual(true) - expect(users[6].bool).toEqual(true) - expect(users[7].bool).toEqual(false) - }) - - it('can accept `null` when there is `nullable` chain', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Bool(true, { nullable: true }) - bool!: boolean - - static fields() { - return { - id: this.attr(null), - bool: this.boolean(true).nullable() - } - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, bool: '' }, - { id: 3, bool: 'string' }, - { id: 4, bool: '0' }, - { id: 5, bool: 0 }, - { id: 6, bool: 1 }, - { id: 7, bool: true }, - { id: 8, bool: null } - ]) - - const users = User.all() - - expect(users[0].bool).toEqual(true) - expect(users[1].bool).toEqual(false) - expect(users[2].bool).toEqual(true) - expect(users[3].bool).toEqual(false) - expect(users[4].bool).toEqual(false) - expect(users[5].bool).toEqual(true) - expect(users[6].bool).toEqual(true) - expect(users[7].bool).toEqual(null) - }) -}) diff --git a/test/feature/attributes/Number.spec.ts b/test/feature/attributes/Number.spec.ts deleted file mode 100644 index 218480af..00000000 --- a/test/feature/attributes/Number.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Attributes – Number', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Num(0) - num!: number - - static fields() { - return { - id: this.attr(null), - num: this.number(0) - } - } - } - - it('casts the value to `Number` when creating data', async () => { - const store = createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, num: 1 }, - { id: 3, num: 1.5 }, - { id: 4, num: '2' }, - { id: 5, num: '2.5' }, - { id: 6, num: true }, - { id: 7, num: false }, - { id: 8, num: null } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, num: 0 }, - 2: { $id: '2', id: 2, num: 1 }, - 3: { $id: '3', id: 3, num: 1.5 }, - 4: { $id: '4', id: 4, num: 2 }, - 5: { $id: '5', id: 5, num: 2.5 }, - 6: { $id: '6', id: 6, num: 1 }, - 7: { $id: '7', id: 7, num: 0 }, - 8: { $id: '8', id: 8, num: 0 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('casts the value to `Number` when retrieving data', async () => { - createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, num: 1 }, - { id: 3, num: '2' }, - { id: 4, num: true }, - { id: 5, num: false }, - { id: 6, num: null } - ]) - - const users = User.all() - - expect(users[0].num).toEqual(0) - expect(users[1].num).toEqual(1) - expect(users[2].num).toEqual(2) - expect(users[3].num).toEqual(1) - expect(users[4].num).toEqual(0) - expect(users[5].num).toEqual(0) - }) - - it('can accept `null` when there is `nullable` chain', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Num(0, { nullable: true }) - num!: number - - static fields() { - return { - id: this.attr(null), - num: this.number(0).nullable() - } - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, num: 1 }, - { id: 3, num: '2' }, - { id: 4, num: true }, - { id: 5, num: false }, - { id: 6, num: null } - ]) - - const users = User.all() - - expect(users[0].num).toEqual(0) - expect(users[1].num).toEqual(1) - expect(users[2].num).toEqual(2) - expect(users[3].num).toEqual(1) - expect(users[4].num).toEqual(0) - expect(users[5].num).toEqual(null) - }) -}) diff --git a/test/feature/attributes/String.spec.ts b/test/feature/attributes/String.spec.ts deleted file mode 100644 index 4e5b11c9..00000000 --- a/test/feature/attributes/String.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Attributes – String', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Str('default') - str!: string - - static fields() { - return { - id: this.attr(null), - str: this.string('default') - } - } - } - - it('casts the value to `String` when creating data', async () => { - const store = createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, str: 'value' }, - { id: 3, str: 1 }, - { id: 4, str: true }, - { id: 5, str: null } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, str: 'default' }, - 2: { $id: '2', id: 2, str: 'value' }, - 3: { $id: '3', id: 3, str: '1' }, - 4: { $id: '4', id: 4, str: 'true' }, - 5: { $id: '5', id: 5, str: 'null' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('casts the value to `String` when retrieving data', async () => { - createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, str: 'value' }, - { id: 3, str: 1 }, - { id: 4, str: true }, - { id: 5, str: null } - ]) - - const users = User.all() - - expect(users[0].str).toEqual('default') - expect(users[1].str).toEqual('value') - expect(users[2].str).toEqual('1') - expect(users[3].str).toEqual('true') - expect(users[4].str).toEqual('null') - }) - - it('can accept `null` when there is `nullable` chain', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Str('default', { nullable: true }) - str!: string - - static fields() { - return { - id: this.attr(null), - str: this.string('default').nullable() - } - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1 }, - { id: 2, str: 'value' }, - { id: 3, str: 1 }, - { id: 4, str: true }, - { id: 5, str: null } - ]) - - const users = User.all() - - expect(users[0].str).toEqual('default') - expect(users[1].str).toEqual('value') - expect(users[2].str).toEqual('1') - expect(users[3].str).toEqual('true') - expect(users[4].str).toEqual(null) - }) -}) diff --git a/test/feature/attributes/Uid.spec.ts b/test/feature/attributes/Uid.spec.ts deleted file mode 100644 index 1854f631..00000000 --- a/test/feature/attributes/Uid.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Uid from '@/support/Uid' -import Model from '@/model/Model' - -describe('Feature – Attributes – Uid', () => { - beforeEach(() => { - Uid.reset() - }) - - describe('without default value', () => { - class User extends Model { - static entity = 'users' - - // @Uid - id!: string - - // @Uid - id2!: string - - static fields() { - return { - id: this.uid(), - id2: this.uid() - } - } - } - - it('generates uid as a default value', async () => { - const store = createStore([{ model: User }]) - - await User.create([{}, {}, {}]) - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', id2: '$uid4' }, - $uid2: { $id: '$uid2', id: '$uid2', id2: '$uid5' }, - $uid3: { $id: '$uid3', id: '$uid3', id2: '$uid6' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('will do nothing if the value exists', async () => { - const store = createStore([{ model: User }]) - - await User.create([ - { id: 1, id2: 'id1' }, - { id: 2, id2: 'id2' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, id2: 'id1' }, - 2: { $id: '2', id: 2, id2: 'id2' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - }) - - describe('with default value', () => { - class User extends Model { - static entity = 'users' - - // @Uid(() => 1) - id!: number - - // @Uid(() => 2) - id2!: number - - static fields() { - return { - id: this.uid(() => 1), - id2: this.uid(() => 2) - } - } - } - - it('generates user provided uid as a default value', async () => { - const store = createStore([{ model: User }]) - - await User.create([{}]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, id2: 2 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - }) -}) diff --git a/test/feature/basics/Delete.spec.ts b/test/feature/basics/Delete.spec.ts deleted file mode 100644 index ba0481ea..00000000 --- a/test/feature/basics/Delete.spec.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Delete', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Str('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Num - id!: number - - static fields() { - return { - id: this.number(null) - } - } - } - - const getStore = () => createStore([{ model: User }, { model: Post }]) - - it('can delete a record by specifying the id', async () => { - const store = getStore() - - await User.create([{ id: 1 }, { id: 2 }]) - - await User.delete(1) - - const expected = createState({ - users: { - 2: { $id: '2', id: 2, name: '' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete a record by specifying id as a string', async () => { - const store = getStore() - - await User.create([{ id: 'string_id_1' }, { id: 'string_id_2' }]) - - await User.delete('string_id_2') - - const expected = createState({ - users: { - string_id_1: { $id: 'string_id_1', id: 'string_id_1', name: '' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete records by specifying a closure', async () => { - const store = getStore() - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Jane Doe' } - ]) - - await User.delete((user) => user.name === 'Jane Doe') - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('does nothing if the specified id does not exist', async () => { - const store = getStore() - - await User.create([{ id: 1 }, { id: 2 }]) - - await User.delete(3) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: '' }, - 2: { $id: '2', id: 2, name: '' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns deleted item', async () => { - const store = getStore() - - await User.create([{ id: 1 }, { id: 2 }]) - - const user = await User.delete(1) - - expect(user).toBeInstanceOf(User) - expect(user?.id).toBe(1) - - const expected = createState({ - users: { - 2: { $id: '2', id: 2, name: '' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns all deleted records as a collection when specifying a closure', async () => { - const store = getStore() - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Jane Doe' } - ]) - - const users = await User.delete((user) => user.name === 'Jane Doe') - - expect(users.length).toBe(2) - expect(users[0]).toBeInstanceOf(User) - expect(users[0].id).toBe(2) - expect(users[1]).toBeInstanceOf(User) - expect(users[1].id).toBe(3) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('deletes itself by instance method', async () => { - const store = getStore() - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const user = User.find(2) as User - - await user.$delete() - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete all records in the entity', async () => { - const store = getStore() - - await User.create([{ id: 1 }, { id: 2 }]) - - await User.deleteAll() - - const expected = createState({ - users: {}, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete all records in the entity by the instance method', async () => { - const store = getStore() - - await User.create([{ id: 1 }, { id: 2 }]) - - await new User().$deleteAll() - - const expected = createState({ - users: {}, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete all records in the entire entities', async () => { - const store = getStore() - - await User.create([{ id: 1 }, { id: 2 }]) - - await Post.create([{ id: 3 }, { id: 4 }]) - - await store.dispatch('entities/deleteAll') - - const expected = createState({ - users: {}, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/basics/Delete_Composite_Key.spec.ts b/test/feature/basics/Delete_Composite_Key.spec.ts deleted file mode 100644 index ba3aa0e9..00000000 --- a/test/feature/basics/Delete_Composite_Key.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Delete Composite Key', () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['first_id', 'second_id'] - - static fields() { - return { - first_id: this.attr(null), - second_id: this.attr(null) - } - } - } - - const getStore = () => createStore([{ model: User }]) - - it('can delete a record by specifying the composite id as an array', async () => { - const store = getStore() - - await User.create([ - { first_id: 1, second_id: 1 }, - { first_id: 1, second_id: 2 } - ]) - - await User.delete([1, 1]) - - const expected = createState({ - users: { - '[1,2]': { $id: '[1,2]', first_id: 1, second_id: 2 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('deletes itself by instance method even when model has composite primary key', async () => { - const store = getStore() - - await User.create([ - { first_id: 1, second_id: 2 }, - { first_id: 3, second_id: 4 } - ]) - - const user = User.find([1, 2]) as User - - await user.$delete() - - const expected = createState({ - users: { - '[3,4]': { $id: '[3,4]', first_id: 3, second_id: 4 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/basics/Exists.spec.ts b/test/feature/basics/Exists.spec.ts deleted file mode 100644 index ba7b056d..00000000 --- a/test/feature/basics/Exists.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Exists', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - } - - it('can check if a database is empty by exists method', async () => { - createStore([{ model: User }]) - - expect(User.exists()).toBe(false) - }) - - it('can check if a database contains data by exists method', async () => { - createStore([{ model: User }]) - - await User.create([{ id: 1 }, { id: 2 }]) - - expect(User.exists()).toBe(true) - }) - - it('can check if a query chain would return data', async () => { - createStore([{ model: User }]) - - await User.create([{ id: 1 }, { id: 2 }]) - - expect( - User.query() - .where('id', 3) - .exists() - ).toBe(false) - expect( - User.query() - .where('id', 1) - .exists() - ).toBe(true) - }) -}) diff --git a/test/feature/basics/InsertOrUpdate.spec.ts b/test/feature/basics/InsertOrUpdate.spec.ts deleted file mode 100644 index 77cbcfb4..00000000 --- a/test/feature/basics/InsertOrUpdate.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Insert Or Update', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - title: this.attr('') - } - } - } - - const getStore = () => createStore([{ model: User }, { model: Post }]) - - it('can insert new data and update existing data', async () => { - const store = getStore() - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - await store.dispatch('entities/posts/create', { - data: { id: 1, user_id: 1, title: 'title 01' } - }) - - await store.dispatch('entities/users/insertOrUpdate', { - data: [ - { - id: 1, - name: 'Jane Doe', - posts: [{ id: 1, user_id: 1, title: 'title 02' }] - }, - { - id: 2, - name: 'Johnny Doe', - posts: [{ id: 2, user_id: 2, title: 'title 03' }] - } - ] - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe', posts: [] }, - 2: { $id: '2', id: 2, name: 'Johnny Doe', posts: [] } - }, - posts: { - 1: { $id: '1', id: 1, user_id: 1, title: 'title 02' }, - 2: { $id: '2', id: 2, user_id: 2, title: 'title 03' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/basics/PersistOptions.spec.ts b/test/feature/basics/PersistOptions.spec.ts deleted file mode 100644 index 5d3c2a85..00000000 --- a/test/feature/basics/PersistOptions.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Persist Options', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.hasMany(Post, 'posts') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - title: this.attr('') - } - } - } - - it('can `create` related records', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await Post.create({ id: 1 }) - - await User.insert( - { - id: 2, - posts: [{ id: 2 }, { id: 3 }] - }, - { create: ['posts'] } - ) - - expect(store.state.entities.users.data['1'].id).toBe(1) - expect(store.state.entities.users.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['1']).toBe(undefined) - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['3'].id).toBe(3) - }) - - it('can `insert` related records', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await Post.create({ id: 1 }) - - await User.create( - { - id: 2, - posts: [{ id: 2 }, { id: 3 }] - }, - { insert: ['posts'] } - ) - - expect(store.state.entities.users.data['1']).toBe(undefined) - expect(store.state.entities.users.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['3'].id).toBe(3) - }) - - it('can `update` related records', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await Post.create([ - { id: 1, title: 'Title 01' }, - { id: 2, title: 'Title 02' } - ]) - - await User.create( - { - id: 2, - posts: [ - { id: 1, title: 'Title 01-Edit' }, - { id: 2, title: 'Title 02-Edit' } - ] - }, - { update: ['posts'] } - ) - - expect(store.state.entities.users.data['1']).toBe(undefined) - expect(store.state.entities.users.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['1'].title).toBe('Title 01-Edit') - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['2'].title).toBe('Title 02-Edit') - }) - - it('can `insertOrUpdate` related records', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await Post.create([ - { id: 1, title: 'Title 01' }, - { id: 2, title: 'Title 02' } - ]) - - await User.create( - { - id: 2, - posts: [ - { id: 2, title: 'Title 02-Edit' }, - { id: 3, title: 'Title 03' } - ] - }, - { insertOrUpdate: ['posts'] } - ) - - expect(store.state.entities.users.data['1']).toBe(undefined) - expect(store.state.entities.users.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['1'].title).toBe('Title 01') - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['2'].title).toBe('Title 02-Edit') - expect(store.state.entities.posts.data['3'].id).toBe(3) - expect(store.state.entities.posts.data['3'].title).toBe('Title 03') - }) -}) diff --git a/test/feature/basics/Retrieve.spec.ts b/test/feature/basics/Retrieve.spec.ts deleted file mode 100644 index 156a5080..00000000 --- a/test/feature/basics/Retrieve.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Retrieve', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - } - - it('can retrieve all records as a model instance by all method', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const expected = [ - { $id: '1', id: 1 }, - { $id: '2', id: 2 } - ] - - const users = store.getters['entities/users/all']() - - expect(users.length).toBe(2) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - expect(users).toEqual(expected) - }) - - it('can retrieve all records by chained all method', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const expected = [ - { $id: '1', id: 1 }, - { $id: '2', id: 2 } - ] - - const users = store.getters['entities/users/query']().all() - - expect(users.length).toBe(2) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - expect(users).toEqual(expected) - }) - - it('can retrieve all records as a model instance by get method', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const expected = [ - { $id: '1', id: 1 }, - { $id: '2', id: 2 } - ] - - const users = store.getters['entities/users/query']().get() - - expect(users.length).toBe(2) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - expect(users).toEqual(expected) - }) - - it('returns empty array when multiple record can not be found', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const users = store.getters['entities/users/query']() - .where('id', 3) - .get() - - expect(users).toEqual([]) - }) - - it('can retrieve a single item by id', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const user = store.getters['entities/users/find'](2) - - expect(user.id).toBe(2) - }) - - it('can retrieve array of items by their ids', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/insert', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) - - const expected = [ - { $id: '1', id: 1 }, - { $id: '3', id: 3 } - ] - - const users1 = store.getters['entities/users/findIn']([1, 3]) - - expect(users1.length).toBe(2) - expect(users1[0]).toBeInstanceOf(User) - expect(users1[1]).toBeInstanceOf(User) - expect(users1).toEqual(expected) - - const users2 = store.getters['entities/users/findIn']([4, 5]) - - expect(users2).toEqual([]) - }) - - it('can retrieve a single item by chained find method', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const expected = { $id: '2', id: 2 } - - const user = store.getters['entities/users/query']().find(2) - - expect(user).toEqual(expected) - }) - - describe('#first', () => { - it('can retrieve the first item from the store', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/insert', { - data: [{ id: 1 }, { id: 2 }] - }) - - const expected = { $id: '1', id: 1 } - - const user = store.getters['entities/users/query']().first() - - expect(user).toEqual(expected) - }) - - it('returns `null` if it can not find any record', () => { - const store = createStore([{ model: User }]) - - const user = store.getters['entities/users/query']().first() - - expect(user).toBe(null) - }) - }) - - describe('#last', () => { - it('can retrieve the last item from the store', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/insert', { - data: [{ id: 1 }, { id: 2 }] - }) - - const expected = { $id: '2', id: 2 } - - const user = store.getters['entities/users/query']().last() - - expect(user).toEqual(expected) - }) - - it('returns `null` if it can not find any record', () => { - const store = createStore([{ model: User }]) - - const user = store.getters['entities/users/query']().last() - - expect(user).toBe(null) - }) - }) - - it('returns null when single item can not be found', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const user = store.getters['entities/users/find'](3) - - expect(user).toBe(null) - }) - - it('returns null when passing `undefined` to the `find` method', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - const user = store.getters['entities/users/find']() - - expect(user).toBe(null) - }) -}) diff --git a/test/feature/basics/Retrieve_LimitAndOffset.spec.ts b/test/feature/basics/Retrieve_LimitAndOffset.spec.ts deleted file mode 100644 index 4de0cb8a..00000000 --- a/test/feature/basics/Retrieve_LimitAndOffset.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Retrieve – Limit And Offset', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - } - - it('can limit number of records', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] - }) - - const expected = [ - { $id: '1', id: 1 }, - { $id: '2', id: 2 } - ] - - const users = store.getters['entities/users/query']() - .limit(2) - .get() - - expect(users).toEqual(expected) - }) - - it('can limit number of records from an offset', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] - }) - - const expected = [ - { $id: '2', id: 2 }, - { $id: '3', id: 3 } - ] - - const users = store.getters['entities/users/query']() - .limit(2) - .offset(1) - .get() - - expect(users).toEqual(expected) - }) -}) diff --git a/test/feature/basics/Retrieve_OrderBy.spec.ts b/test/feature/basics/Retrieve_OrderBy.spec.ts deleted file mode 100644 index a9736144..00000000 --- a/test/feature/basics/Retrieve_OrderBy.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Retrieve – Order By', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - it('can sort by model fields in asc order', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John' }, - { id: 2, name: 'Andy' }, - { id: 3, name: 'Roger' }, - { id: 10, name: 'Andy' } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John' }, - { $id: '2', id: 2, name: 'Andy' }, - { $id: '3', id: 3, name: 'Roger' }, - { $id: '10', id: 10, name: 'Andy' } - ] - - const result1 = store.getters['entities/users/query']() - .orderBy('id') - .get() - expect(result1).toEqual(expected) - - const result2 = store.getters['entities/users/query']() - .orderBy('id', 'asc') - .get() - expect(result2).toEqual(expected) - }) - - it('can sort by model fields in desc order', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John' }, - { id: 2, name: 'Andy' }, - { id: 3, name: 'Roger' }, - { id: 10, name: 'Andy' } - ] - }) - - const expected = [ - { $id: '10', id: 10, name: 'Andy' }, - { $id: '2', id: 2, name: 'Andy' }, - { $id: '1', id: 1, name: 'John' }, - { $id: '3', id: 3, name: 'Roger' } - ] - - const result = store.getters['entities/users/query']() - .orderBy('name') - .orderBy('id', 'desc') - .get() - - expect(result).toEqual(expected) - }) - - it('can sort using a function ', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John' }, - { id: 2, name: 'Andy' }, - { id: 3, name: 'Roger' } - ] - }) - - const expected = [ - { $id: '2', id: 2, name: 'Andy' }, - { $id: '3', id: 3, name: 'Roger' }, - { $id: '1', id: 1, name: 'John' } - ] - - const result = store.getters['entities/users/query']() - .orderBy((user: User) => user.name[2]) // Sort by third character in name. - .get() - - expect(result).toEqual(expected) - }) - - it('can sort using a function in desc order', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John' }, - { id: 2, name: 'Andy' } - ] - }) - - const expected = [ - { $id: '2', id: 2, name: 'Andy' }, - { $id: '1', id: 1, name: 'John' } - ] - - const result = store.getters['entities/users/query']() - .orderBy((user: User) => user.id, 'desc') - .get() - - expect(result).toEqual(expected) - }) - - it('can sort by model fields with first method', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John' }, - { id: 2, name: 'Andy' }, - { id: 3, name: 'Roger' } - ] - }) - - const expected = { $id: '2', id: 2, name: 'Andy' } - - const result = store.getters['entities/users/query']() - .orderBy('name') - .orderBy('id', 'desc') - .first() - - expect(result).toEqual(expected) - }) -}) diff --git a/test/feature/basics/Retrieve_SSRSupport.spec.ts b/test/feature/basics/Retrieve_SSRSupport.spec.ts deleted file mode 100644 index f6e30713..00000000 --- a/test/feature/basics/Retrieve_SSRSupport.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Retrieve – SSR Support', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - - isActive() { - return true - } - } - - const users = { - mutations: { - setUsers(state: any, users: any) { - state.data = users - } - } - } - - it('will instantiate pre hydrated records when fetching', () => { - const store = createStore([{ model: User, module: users }]) - - store.commit('entities/users/setUsers', [{ id: 1 }, { id: 2 }]) - - const records = User.all() - - expect(records[0]).toBeInstanceOf(User) - expect(records[1]).toBeInstanceOf(User) - }) - - it('can access record as Model instance inside retrieve callback with pre hydrated records', () => { - const store = createStore([{ model: User, module: users }]) - - store.commit('entities/users/setUsers', [{ id: 1 }, { id: 2 }]) - - const records = User.query() - .where((user: User) => user.isActive()) - .get() - - expect(records[0]).toBeInstanceOf(User) - expect(records[1]).toBeInstanceOf(User) - }) -}) diff --git a/test/feature/basics/Retrieve_Where.spec.ts b/test/feature/basics/Retrieve_Where.spec.ts deleted file mode 100644 index 65e7698a..00000000 --- a/test/feature/basics/Retrieve_Where.spec.ts +++ /dev/null @@ -1,519 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Retrieve – Where', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: boolean - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - // @Attribute(false) - active!: boolean - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null), - active: this.attr(false) - } - } - - isActive() { - return this.active - } - } - - it('can retrieve records that matches the where clause', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .where('age', 20) - .where('active', true) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereId clause', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [{ $id: '2', id: 2, name: 'Jane', age: 20, active: true }] - - const users = store.getters['entities/users/query']() - .whereId(2) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereId clause using intersection ("and" boolean)', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const users = store.getters['entities/users/query']() - .whereId(2) - .whereId(1) - .get() - - expect(users).toEqual([]) - }) - - it('can retrieve records that matches the whereId and orWhere', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .whereId(2) - .orWhere('id', 1) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereIdIn clause', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [ - { $id: '2', id: 2, name: 'Jane', age: 20, active: true }, - { $id: '3', id: 3, name: 'Johnny', age: 20, active: false } - ] - - const users = store.getters['entities/users/query']() - .whereIdIn([2, 3]) - .all() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereIdIn clause using intersection ("and" boolean)', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [{ $id: '2', id: 2, name: 'Jane', age: 20, active: true }] - - const users = store.getters['entities/users/query']() - .whereIdIn([2, 3]) - .whereIdIn([1, 2]) - .all() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereIdIn and orWhere', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true }, - { $id: '3', id: 3, name: 'Johnny', age: 20, active: false } - ] - - const users = store.getters['entities/users/query']() - .whereIdIn([2, 3]) - .orWhere('id', 1) - .all() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that contains the value in the where clause array', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 24, active: true }, - { id: 3, name: 'Johnny', age: 20, active: false } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 24, active: true } - ] - - const users = store.getters['entities/users/query']() - .where('age', [20, 24]) - .where('active', true) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches in the where clause value as a closure', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .where('age', (value: number) => value === 20) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches in the where clause as a closure', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .where((user: User) => user.age === 20) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the where query with a function that accesses variables from outside the scope', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const ageAsVariable = 20 - - const users = store.getters['entities/users/query']() - .where((user: User) => user.age === ageAsVariable) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the where query with nested query builder', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .where((_user: User, query: Query) => { - query.where('age', 20) - }) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that match the where query by comparing model instance.', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 24, active: false }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: false } - ] - }) - - const expected = [{ $id: '2', id: 2, name: 'Jane', age: 20, active: true }] - - const users = store.getters['entities/users/query']() - .where((_user: User, _query: Query, model: User) => { - return model.isActive() - }) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the where query with complex nested query builder', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 24, active: false }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 24, active: false }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .where((_user: User, query: Query) => { - query.where('age', 20).where('active', true) - }) - .orWhere('id', 1) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the orWhere query', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 20, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - const users = store.getters['entities/users/query']() - .where('id', 1) - .orWhere('id', 2) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records with only the orWhere query', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 20, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true } - ] - }) - - const expected = [{ $id: '2', id: 2, name: 'Jane', age: 20, active: true }] - - const users = store.getters['entities/users/query']() - .orWhere('id', 2) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the where query with 2 nested query builders, starting with orWhere', async () => { - const store = createStore([ - { - model: User - } - ]) - - await store.dispatch('entities/users/create', { - data: [ - { - id: 1, - name: 'John', - age: 24, - active: false - }, - { - id: 2, - name: 'Jane', - age: 20, - active: true - }, - { - id: 3, - name: 'Johnny', - age: 24, - active: true - } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 24, active: false }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - // (A && B) || (C && D) - const users = store.getters['entities/users/query']() - .where((_user: User, query: Query) => { - query.where('age', 20).where('active', true) - }) - .orWhere((_user: User, query: Query) => { - query.where('age', 24).where('active', false) - }) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the where query with 2 nested query builders, starting with where', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 21, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true }, - { id: 4, name: 'James', age: 21, active: false } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 21, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true } - ] - - // (A || B) && (C || D) - const users = store.getters['entities/users/query']() - .where((_user: User, query: Query) => { - query.where('age', 20).orWhere('age', 21) - }) - .where((_user: User, query: Query) => { - query.where('active', true) - }) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the where query several nested query builders', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John', age: 21, active: true }, - { id: 2, name: 'Jane', age: 20, active: true }, - { id: 3, name: 'Johnny', age: 24, active: true }, - { id: 4, name: 'James', age: 21, active: false }, - { id: 5, name: 'Jimmy', age: 22, active: true }, - { id: 6, name: 'Jim', age: 22, active: true }, - { id: 7, name: 'Jules', age: 22, active: false } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John', age: 21, active: true }, - { $id: '2', id: 2, name: 'Jane', age: 20, active: true }, - { $id: '5', id: 5, name: 'Jimmy', age: 22, active: true } - ] - - // (A || B || (E && F)) && (C || D) - const users = store.getters['entities/users/query']() - .where((_user: User, query: Query) => { - query - .where('age', 20) - .orWhere('age', 21) - .orWhere((_user2: User, q: Query) => { - q.where('age', 22).where((u: User) => u.name.indexOf('Jimm') === 0) - }) - }) - .where((_user: User, query: Query) => { - query.where('active', true) - }) - .get() - - expect(users).toEqual(expected) - }) -}) diff --git a/test/feature/basics/Retrieve_Where_Composite_Key.spec.ts b/test/feature/basics/Retrieve_Where_Composite_Key.spec.ts deleted file mode 100644 index 5c41d0b7..00000000 --- a/test/feature/basics/Retrieve_Where_Composite_Key.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Retrieve – Where - Composite Keys', () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['key_1', 'key_2'] - - static fields() { - return { - key_1: this.attr(null), - key_2: this.attr(null) - } - } - } - - it('can retrieve records that matches the whereId clause', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { key_1: 1, key_2: 2 }, - { key_1: 2, key_2: 2 } - ] - }) - - const expected = { $id: '[1,2]', key_1: 1, key_2: 2 } - - const user = store.getters['entities/users/query']() - .whereId([1, 2]) - .first() - - expect(user).toEqual(expected) - }) - - it('can retrieve records that matches the whereId clause using intersection ("and" boolean)', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { key_1: 1, key_2: 2 }, - { key_1: 2, key_2: 2 } - ] - }) - - const users = store.getters['entities/users/query']() - .whereId([1, 2]) - .whereId([2, 2]) - .get() - - expect(users).toEqual([]) - }) - - it('can retrieve records that matches the whereId and orWhere', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { key_1: 1, key_2: 2 }, - { key_1: 2, key_2: 2 } - ] - }) - - const expected = [ - { $id: '[1,2]', key_1: 1, key_2: 2 }, - { $id: '[2,2]', key_1: 2, key_2: 2 } - ] - - const users = store.getters['entities/users/query']() - .whereId([1, 2]) - .orWhere('key_1', 2) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereIdIn clause', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { key_1: 1, key_2: 2 }, - { key_1: 2, key_2: 2 }, - { key_1: 3, key_2: 2 } - ] - }) - - const expected = [ - { $id: '[2,2]', key_1: 2, key_2: 2 }, - { $id: '[3,2]', key_1: 3, key_2: 2 } - ] - - const users = store.getters['entities/users/query']() - .whereIdIn([ - [2, 2], - [3, 2] - ]) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereIdIn clause using intersection ("and" boolean)', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { key_1: 1, key_2: 2 }, - { key_1: 2, key_2: 2 }, - { key_1: 3, key_2: 2 } - ] - }) - - const expected = [{ $id: '[2,2]', key_1: 2, key_2: 2 }] - - const users = store.getters['entities/users/query']() - .whereIdIn([ - [2, 2], - [3, 2] - ]) - .whereIdIn([ - [1, 2], - [2, 2] - ]) - .get() - - expect(users).toEqual(expected) - }) - - it('can retrieve records that matches the whereIdIn and orWhere', async () => { - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { key_1: 1, key_2: 2 }, - { key_1: 2, key_2: 2 }, - { key_1: 3, key_2: 2 } - ] - }) - - const expected = [ - { $id: '[1,2]', key_1: 1, key_2: 2 }, - { $id: '[2,2]', key_1: 2, key_2: 2 }, - { $id: '[3,2]', key_1: 3, key_2: 2 } - ] - - const users = store.getters['entities/users/query']() - .whereIdIn([ - [2, 2], - [3, 2] - ]) - .orWhere('key_1', 1) - .get() - - expect(users).toEqual(expected) - }) - - it('throws error when passing value other than an array to `whereId`', () => { - const store = createStore([{ model: User }]) - - expect(() => { - store.getters['entities/users/query']() - .whereId(2) - .get() - }).toThrowError('[Vuex ORM]') - }) - - it('throws error when passing value other than a nested array to `whereIdIn`', () => { - const store = createStore([{ model: User }]) - - expect(() => { - store.getters['entities/users/query']() - .whereIdIn([2, 2]) - .get() - }).toThrowError('[Vuex ORM]') - }) -}) diff --git a/test/feature/basics/State.spec.ts b/test/feature/basics/State.spec.ts deleted file mode 100644 index f6c69e67..00000000 --- a/test/feature/basics/State.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – State', () => { - it('can define state at the model', () => { - class User extends Model { - static entity = 'users' - - static state = { - fetching: false - } - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - expect(store.state.entities.users.fetching).toBe(false) - }) - - it('can define state at the model as a function', () => { - class User extends Model { - static entity = 'users' - - static state() { - return { - fetching: false - } - } - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - expect(store.state.entities.users.fetching).toBe(false) - }) - - it('can mutate the state from the model', () => { - class User extends Model { - static entity = 'users' - - static state = { - fetching: false - } - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - User.commit((state) => { - state.fetching = true - }) - - expect(store.state.entities.users.fetching).toBe(true) - }) - - it('can access model instance within mutation handler', () => { - class User extends Model { - static entity = 'users' - - static state = { - fetching: false - } - - static fields() { - return { - id: this.attr(null) - } - } - - static fetching(state: any) { - state.fetching = true - } - } - - const store = createStore([{ model: User }]) - - User.commit((state) => { - User.fetching(state) - }) - - expect(store.state.entities.users.fetching).toBe(true) - }) -}) diff --git a/test/feature/basics/Update.spec.ts b/test/feature/basics/Update.spec.ts deleted file mode 100644 index 5fee0347..00000000 --- a/test/feature/basics/Update.spec.ts +++ /dev/null @@ -1,690 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Basics – Update', () => { - it('can update record by including primary key in the data', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 0, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { - data: { id: 0, age: 24 } - }) - - const user = store.getters['entities/users/find'](0) - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(24) - }) - - it('can update record by including custom primary key in the data', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = 'user_id' - - static fields() { - return { - user_id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { user_id: 1, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { user_id: 1, age: 24 }) - - const user = store.state.entities.users.data['1'] - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(24) - }) - - it('can update record by including composite primary key in the data', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['key_1', 'key_2'] - - static fields() { - return { - key_1: this.attr(null), - key_2: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { key_1: 1, key_2: 2, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { - key_1: 1, - key_2: 2, - age: 24 - }) - - const user = store.getters['entities/users/query']() - .where('key_1', 1) - .where('key_2', 2) - .first() - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(24) - }) - - it('can update record by passing an array', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 0, name: 'John Doe', age: 30 }, - { id: 1, name: 'Jane Doe', age: 24 } - ] - }) - - await store.dispatch('entities/users/update', [ - { id: 0, age: 24 }, - { id: 1, age: 30 } - ]) - - const user0 = store.getters['entities/users/find'](0) - const user1 = store.getters['entities/users/find'](1) - - expect(user0.name).toBe('John Doe') - expect(user0.age).toBe(24) - expect(user1.name).toBe('Jane Doe') - expect(user1.age).toBe(30) - }) - - it('can update record by passing an array as a data', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 0, name: 'John Doe', age: 30 }, - { id: 1, name: 'Jane Doe', age: 24 } - ] - }) - - await store.dispatch('entities/users/update', { - data: [ - { id: 0, age: 24 }, - { id: 1, age: 30 } - ] - }) - - const user0 = store.getters['entities/users/find'](0) - const user1 = store.getters['entities/users/find'](1) - - expect(user0.name).toBe('John Doe') - expect(user0.age).toBe(24) - expect(user1.name).toBe('Jane Doe') - expect(user1.age).toBe(30) - }) - - it('can update record by specifying condition with id', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { - where: 1, - data: { age: 24 } - }) - - const user = store.state.entities.users.data['1'] - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(24) - }) - - it('can update record by specifying condition with id of string', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { - where: '1', - data(user: any) { - user.age = 24 - } - }) - - const user = store.state.entities.users.data['1'] - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(24) - }) - - it('can update record by specifying condition with composite id', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['key_1', 'key_2'] - - static fields() { - return { - key_1: this.attr(null), - key_2: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { key_1: 1, key_2: 2, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { - data: { age: 24 }, - where: [1, 2] - }) - - const user = store.getters['entities/users/query']().first() - expect(user.key_1).toBe(1) - expect(user.key_2).toBe(2) - expect(user.age).toBe(24) - }) - - it('can update record by specifying condition with closure', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'JD', age: 30 }, - { id: 2, name: 'JD', age: 30 }, - { id: 3, name: 'Johnny', age: 20 } - ] - }) - - await store.dispatch('entities/users/update', { - where(record: any) { - return record.name === 'JD' - }, - data: { age: 24 } - }) - - const users = store.state.entities.users.data - - expect(users['1'].age).toBe(24) - expect(users['2'].age).toBe(24) - expect(users['3'].age).toBe(20) - }) - - it('can update record by specifying data with closure', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'JD', age: 30 }, - { id: 2, name: 'JD', age: 30 }, - { id: 3, name: 'Johnny', age: 20 } - ] - }) - - await store.dispatch('entities/users/update', { - where: 1, - data(record: any) { - record.name = 'John Doe' - record.age = 24 - } - }) - - const user = store.getters['entities/users/find'](1) - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(24) - }) - - it('does nothing if the specified id can not be found', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1, name: 'JD', age: 30 }] - }) - - await store.dispatch('entities/users/update', { - where: 2, - data(user: any) { - user.name = 'John Doe' - user.age = 24 - } - }) - - const user = store.getters['entities/users/find'](2) - - expect(user).toBe(null) - }) - - it('can update record by specifying data and where with closure', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'JD', age: 30 }, - { id: 2, name: 'JD', age: 30 }, - { id: 3, name: 'Johnny', age: 20 } - ] - }) - - await store.dispatch('entities/users/update', { - where(record: any) { - return record.name === 'JD' - }, - - data(record: any) { - record.age = 24 - } - }) - - const users = store.getters['entities/users/all']() - - expect(users[0].age).toBe(24) - expect(users[1].age).toBe(24) - expect(users[2].age).toBe(20) - }) - - it('can update array field', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - parameters: this.attr([]) - } - } - } - - const store = createStore([{ model: User }]) - - await await store.dispatch('entities/users/create', { - data: { id: 1, parameters: [1, 2] } - }) - - await store.dispatch('entities/users/update', { - id: 1, - parameters: [3] - }) - - const user = store.getters['entities/users/find'](1) - - expect(user.parameters).toEqual([3]) - }) - - it('can update object field', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - settings: this.attr({}) - } - } - } - - const store = createStore([{ model: User }]) - - await await store.dispatch('entities/users/create', { - data: { id: 1, settings: { role: 'admin' } } - }) - - await store.dispatch('entities/users/update', { - id: 1, - settings: { active: true } - }) - - const user = store.getters['entities/users/find'](1) - - expect(user.settings).toEqual({ active: true }) - }) - - it('ignores field which is not defined at model schema', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - const store = createStore([{ model: User }]) - - await await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - await store.dispatch('entities/users/update', { - id: 1, - name: 'Jane Doe', - age: 24 - }) - - const user = store.getters['entities/users/find'](1) - - expect(user.name).toBe('Jane Doe') - expect(user.age).toBe(undefined) - }) - - it('does nothing if the condition did not match', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe', age: 30 } - }) - - await store.dispatch('entities/users/update', { id: 2, age: 24 }) - await store.dispatch('entities/users/update', { user_id: 2, age: 24 }) - - const user = store.getters['entities/users/find'](1) - - expect(user.name).toBe('John Doe') - expect(user.age).toBe(30) - }) - - it('throws error when the `data` is closure and there is no condition', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe', age: 30 } - }) - - try { - await store.dispatch('entities/users/update', { - data(user: any) { - user.age = 24 - } - }) - } catch (e) { - expect(e.message).toBe( - 'You must specify `where` to update records by specifying `data` as a closure.' - ) - } - }) - - it('throws error when the condition is specified but model has composite key defined', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['id1', 'id2'] - - static fields() { - return { - id1: this.attr(null), - id2: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id1: 1, id2: 2, name: 'John Doe', age: 30 } - }) - - try { - await store.dispatch('entities/users/update', { - where: 1, - data: { age: 24 } - }) - } catch (e) { - expect(e.message).toBe( - "[Vuex ORM] You can't specify `where` value as `string` or `number` " + - 'when you have a composite key defined in your model. Please include ' + - 'composite keys to the `data` fields.' - ) - } - }) - - it('updates index key when primary key gets updated', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - await store.dispatch('entities/users/update', { - where: 1, - data: { id: 2 } - }) - - const user: any = User.find(2) - - expect(user.$id).toBe('2') - expect(user.id).toBe(2) - }) - - it('returns a updated object', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe', age: 30 } - }) - - const collection = await store.dispatch('entities/users/update', { - id: 1, - age: 24 - }) - - expect(collection.users[0]).toBeInstanceOf(User) - expect(collection.users[0].age).toBe(24) - }) - - it('returns a updated object with specifying where condition', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe', age: 30 } - }) - - const result = await store.dispatch('entities/users/update', { - where: 1, - data: { age: 24 } - }) - - expect(result).toBeInstanceOf(User) - expect(result.age).toBe(24) - }) -}) diff --git a/test/feature/database/RegisterModel.spec.ts b/test/feature/database/RegisterModel.spec.ts deleted file mode 100644 index 6b691ef5..00000000 --- a/test/feature/database/RegisterModel.spec.ts +++ /dev/null @@ -1,198 +0,0 @@ -import Vue from 'vue' -import Vuex from 'vuex' -import VuexORM from '@/index' -import Database from '@/database/Database' -import Model from '@/model/Model' - -Vue.use(Vuex) - -describe('Feature – Database - Register Model', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.uid(), - name: this.string('') - } - } - } - - class Hobby extends Model { - static entity = 'hobbies' - - static fields() { - return { - id: this.uid(), - user_id: this.attr(null), - name: this.string(''), - user: this.belongsTo(User, 'user_id') - } - } - } - - const userModule = { - state: { - currentId: null - }, - - getters: { - current: (state: any) => () => state.data[state.currentId] - }, - - mutations: { - setCurrent(state: any, id: number) { - state.currentId = id - } - }, - - actions: { - login({ commit, state }: any, name: string) { - const id = Object.keys(state.data).find( - (key) => state.data[key].name === name - ) - commit('setCurrent', id) - }, - - logout({ commit }: any) { - commit('setCurrent', null) - } - } - } - - it('can register models before being installed to vuex.', async () => { - const database = new Database() - - database.register(User) - database.register(Hobby) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)], - strict: true - }) - - await store.dispatch('entities/hobbies/create', { - data: { - name: 'my hobby', - user: { - name: 'my name' - } - } - }) - - const hobby = store.getters['entities/hobbies/query']() - .with('user') - .first() - const user = store.getters['entities/users/query']().first() - - expect(hobby.name).toEqual('my hobby') - expect(hobby.user.name).toEqual('my name') - expect(user.name).toEqual('my name') - }) - - it('can register modules before being installed to vuex.', async () => { - const database = new Database() - - database.register(User, userModule) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)], - strict: true - }) - - await store.dispatch('entities/users/create', { - data: [{ name: 'user 1' }, { name: 'user 2' }, { name: 'user 3' }] - }) - - const user = store.getters['entities/users/query']() - .where('name', 'user 2') - .first() - const getCurrentUser = store.getters['entities/users/current'] - - expect(getCurrentUser()).toBeUndefined() - - await store.dispatch('entities/users/login', 'user 2') - expect(getCurrentUser()).toEqual(user) - - await store.dispatch('entities/users/logout') - expect(getCurrentUser()).toBeUndefined() - }) - - it('can register models after being installed to vuex.', async () => { - const database = new Database() - - database.register(User) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)], - strict: true - }) - - database.register(Hobby) - - await store.dispatch('entities/hobbies/create', { - data: { - name: 'my hobby', - user: { - name: 'my name' - } - } - }) - - const hobby = store.getters['entities/hobbies/query']() - .with('user') - .first() - const user = store.getters['entities/users/query']().first() - - expect(hobby.name).toEqual('my hobby') - expect(hobby.user.name).toEqual('my name') - expect(user.name).toEqual('my name') - }) - - it('can register modules after being installed to vuex.', async () => { - const database = new Database() - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)], - strict: true - }) - - database.register(User, userModule) - - await store.dispatch('entities/users/create', { - data: [{ name: 'user 1' }, { name: 'user 2' }, { name: 'user 3' }] - }) - - const user = store.getters['entities/users/query']() - .where('name', 'user 2') - .first() - const getCurrentUser = store.getters['entities/users/current'] - - expect(getCurrentUser()).toBeUndefined() - - await store.dispatch('entities/users/login', 'user 2') - expect(getCurrentUser()).toEqual(user) - - await store.dispatch('entities/users/logout') - expect(getCurrentUser()).toBeUndefined() - }) - - it('preserves state of previously registered models', async () => { - const database = new Database() - - database.register(User) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)], - strict: true - }) - - await store.dispatch('entities/users/create', { data: { name: 'my name' } }) - - database.register(Hobby) - - const user = store.getters['entities/users/query']().first() - - expect(user.name).toEqual('my name') - }) -}) diff --git a/test/feature/hooks/Global.spec.ts b/test/feature/hooks/Global.spec.ts deleted file mode 100644 index 2b08fd76..00000000 --- a/test/feature/hooks/Global.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Hooks – Global', () => { - beforeEach(() => { - Query.hooks = {} - Query.lastHookId = 0 - }) - - it('can get instance context in hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1, role: 'admin' }, - { id: 2, role: 'admin' }, - { id: 3, role: 'user' } - ]) - - const expected = [{ $id: '1', id: 1, role: 'admin' }] - - const callbackFunction = function(this: Query, records: any) { - expect(records.length).toBe(2) - expect(this).toBeInstanceOf(Query) - - return records.filter((r: User) => r.id === 1) - } - - const hookId = Query.on('afterWhere', callbackFunction) - - const users = User.query() - .where('role', 'admin') - .get() - expect(users).toEqual(expected) - - const removeBoolean = Query.off(hookId) - expect(removeBoolean).toBe(true) - }) - - it('can remove callback hooks through off method', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - createStore([{ model: User }]) - - await User.create({ - data: [ - { id: 1, role: 'admin' }, - { id: 2, role: 'admin' }, - { id: 3, role: 'user' } - ] - }) - - const callbackFunction = (records: any) => records - - const persistedHookId1 = Query.on('afterWhere', callbackFunction) - expect(Query.hooks.afterWhere.length).toBe(1) - - User.all() - expect(Query.hooks.afterWhere.length).toBe(1) - - const persistedHookId2 = Query.on('beforeSelect', callbackFunction) - expect(Query.hooks.beforeSelect.length).toBe(1) - - User.all() - expect(Query.hooks.beforeSelect.length).toBe(1) - - const removed1 = Query.off(persistedHookId1) - expect(removed1).toBe(true) - expect(Query.hooks.afterWhere.length).toBe(0) - - const removed2 = Query.off(persistedHookId2) - expect(removed2).toBe(true) - expect(Query.hooks.beforeSelect.length).toBe(0) - }) -}) diff --git a/test/feature/hooks/Global_Create.spec.ts b/test/feature/hooks/Global_Create.spec.ts deleted file mode 100644 index 7c4d655f..00000000 --- a/test/feature/hooks/Global_Create.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Hooks – Global Create', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - - Query.hooks = {} - Query.lastHookId = 0 - }) - - it('can process `beforeCreate` hook', async () => { - Query.on('beforeCreate', (model: any) => { - model.role = 'admin' - }) - - await User.create([ - { id: 1, role: 'admin' }, - { id: 2, role: 'user' } - ]) - - const users = User.all() - - expect(users[0].role).toBe('admin') - expect(users[1].role).toBe('admin') - }) - - it('can process multiple `beforeCreate` hook', async () => { - Query.on('beforeCreate', (model: any, _entity: any) => { - model.role = 'admin' - }) - Query.on('beforeCreate', (model: any, _entity: any) => { - model.role = 'not admin' - }) - - await User.create([ - { id: 1, role: 'admin' }, - { id: 2, role: 'user' } - ]) - - const users = User.all() - - expect(users[0].role).toBe('not admin') - expect(users[1].role).toBe('not admin') - }) - - it('can cancel the mutation by returning false from `beforeCreate` hook', async () => { - Query.on('beforeCreate', (model: any, _entity: any): any => { - if (model.role === 'admin') { - return false - } - }) - - await User.create([ - { id: 1, role: 'admin' }, - { id: 2, role: 'user' } - ]) - - const users = User.all() - - expect(users.length).toBe(1) - expect(users[0].role).toBe('user') - }) - - it('can process `afterCreate` hook', async () => { - let hit = false - - Query.on('afterCreate', (_model: any, _entity: any) => { - hit = true - }) - - await User.create({ id: 1, role: 'admin' }) - - expect(hit).toBe(true) - }) -}) diff --git a/test/feature/hooks/Global_Delete.spec.ts b/test/feature/hooks/Global_Delete.spec.ts deleted file mode 100644 index 5c8c4147..00000000 --- a/test/feature/hooks/Global_Delete.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Hooks – Global Delete', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - - Query.hooks = {} - Query.lastHookId = 0 - }) - - it('can process `beforeDelete` hook', async () => { - let hit = false - - Query.on('beforeDelete', (_model: any, _entity: any) => { - hit = true - }) - - await User.create({ id: 1, role: 'user' }) - - await User.delete(1) - - const users = User.all() - - expect(hit).toBe(true) - expect(users).toEqual([]) - }) - - it('can cancel the mutation by returning false from `beforeDelete` hook', async () => { - Query.on('beforeDelete', (_model: any, _entity: any): any => { - return false - }) - - await User.create({ id: 1, role: 'user' }) - - await User.delete(1) - - const user = User.find(1) - - expect(user?.role).toBe('user') - }) - - it('can process `afterDelete` hook', async () => { - let hit = false - - Query.on('afterDelete', (_model: any, _entity: any) => { - hit = true - }) - - await User.create({ id: 1, role: 'user' }) - - await User.delete(1) - - expect(hit).toBe(true) - }) -}) diff --git a/test/feature/hooks/Global_Select.spec.ts b/test/feature/hooks/Global_Select.spec.ts deleted file mode 100644 index cfde7a34..00000000 --- a/test/feature/hooks/Global_Select.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Hooks – Global Select', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - - Query.hooks = {} - Query.lastHookId = 0 - }) - - it('can process afterWhere hook', async () => { - await User.create({ - data: [ - { id: 1, role: 'admin' }, - { id: 2, role: 'admin' }, - { id: 3, role: 'user' } - ] - }) - - const expected = [{ $id: '1', id: 1, role: 'admin' }] - - const hookId = Query.on('afterWhere', function(records: any, _entity: any) { - expect(records.length).toBe(2) - - return records.filter((record: User) => record.id === 1) - }) - - const users = User.query() - .where('role', 'admin') - .get() - expect(users).toEqual(expected) - - const removeBoolean = Query.off(hookId) - expect(removeBoolean).toBe(true) - }) -}) diff --git a/test/feature/hooks/Global_Update.spec.ts b/test/feature/hooks/Global_Update.spec.ts deleted file mode 100644 index 2cf4ad43..00000000 --- a/test/feature/hooks/Global_Update.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Hooks – Global Update', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - - Query.hooks = {} - Query.lastHookId = 0 - }) - - it('can process `beforeUpdate` hook', async () => { - Query.on('beforeUpdate', (model: any, _entity: any) => { - model.role = 'admin' - }) - - await User.create({ id: 1, role: 'user' }) - - await User.update({ id: 1, role: 'guest' }) - - const user = User.find(1) - - expect(user?.role).toBe('admin') - }) - - it('can cancel the mutation by returning false from `beforeUpdate` hook', async () => { - Query.on('beforeUpdate', (model: any, _entity: any): any => { - if (model.role === 'admin') { - return false - } - }) - - await User.create({ id: 1, role: 'user' }) - - await User.update({ id: 1, role: 'admin' }) - - const user = User.find(1) - - expect(user?.role).toBe('user') - }) - - it('can process `afterUpdate` hook', async () => { - let hit = false - - Query.on('afterUpdate', (_model: any, _entity: any) => { - hit = true - }) - - await User.create({ id: 1, role: 'user' }) - - await User.update({ id: 1, role: 'admin' }) - - expect(hit).toBe(true) - }) -}) diff --git a/test/feature/hooks/Local_Binding.spec.ts b/test/feature/hooks/Local_Binding.spec.ts deleted file mode 100644 index f0e2e193..00000000 --- a/test/feature/hooks/Local_Binding.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Hooks – Local Binding', () => { - it('should be scoped to the model class', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(user: User) { - this.setAge(user) - } - - static setAge(user: User) { - user.age = 30 - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - - const expected = { $id: '1', id: 1, name: 'John Doe', age: 30 } - - expect(User.find(1)).toEqual(expected) - }) -}) diff --git a/test/feature/hooks/Local_Create.spec.ts b/test/feature/hooks/Local_Create.spec.ts deleted file mode 100644 index ae91f40a..00000000 --- a/test/feature/hooks/Local_Create.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Hooks – Local Create', () => { - it('can dispatch the `beforeCreate` hook that modifies the data being created', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(record: User) { - record.age = 30 - } - } - - createStore([{ model: User }]) - - await User.create({ id: 1, name: 'John Doe', age: 20 }) - - const expected = { $id: '1', id: 1, name: 'John Doe', age: 30 } - - expect(User.find(1)).toEqual(expected) - }) - - it('it will create the record as is if the `beforeCreate` hook returns nothing', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(_record: User) { - // Do nothing. - } - } - - createStore([{ model: User }]) - - await User.create({ id: 1, name: 'John Doe', age: 20 }) - - const expected = { $id: '1', id: 1, name: 'John Doe', age: 20 } - - expect(User.find(1)).toEqual(expected) - }) - - it('can cancel the create by returning false from `beforeCreate` hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(record: User): boolean | void { - if (record.age === 20) { - return false - } - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1, name: 'John Doe', age: 20 }, - { id: 2, name: 'Jane Doe', age: 24 } - ]) - - const users = User.all() - - expect(users.length).toBe(1) - expect(users[0].id).toBe(2) - }) - - it('can dispatch the `afterCreate` hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static afterCreate(model: User) { - expect(model).toBeInstanceOf(User) - expect(model.id).toBe(1) - } - } - - createStore([{ model: User }]) - - await User.create({ id: 1, name: 'John Doe', age: 20 }) - }) -}) diff --git a/test/feature/hooks/Local_Delete.spec.ts b/test/feature/hooks/Local_Delete.spec.ts deleted file mode 100644 index fc219989..00000000 --- a/test/feature/hooks/Local_Delete.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Hooks – Local Delete', () => { - it('can dispatch the `beforeDelete` hook', async () => { - let hit = false - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - static beforeDelete() { - hit = true - } - } - - createStore([{ model: User }]) - - await User.create({ id: 1, name: 'John Doe' }) - - await User.delete(1) - - expect(hit).toBe(true) - }) - - it('can cancel the delete by returning false from the `beforeDelete` hook', async () => { - let hit = false - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - static beforeDelete(record: User) { - hit = true - - return !(record.name === 'Jane Doe') - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - await User.delete(2) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(hit).toBe(true) - expect(User.all()).toEqual(expected) - }) - - it('can dispatch the `afterDelete` hook', async () => { - let hit = false - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - static afterDelete() { - hit = true - } - } - - createStore([{ model: User }]) - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - await User.delete(2) - - const expected = [{ $id: '1', id: 1, name: 'John Doe' }] - - expect(hit).toBe(true) - expect(User.all()).toEqual(expected) - }) -}) diff --git a/test/feature/hooks/Local_Insert.spec.ts b/test/feature/hooks/Local_Insert.spec.ts deleted file mode 100644 index 54f6a07a..00000000 --- a/test/feature/hooks/Local_Insert.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Hooks – Local Insert', () => { - it('can dispatch the `beforeCreate` hook that modifies the data being inserted', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(record: User) { - record.age = 30 - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - - const expected = { $id: '1', id: 1, name: 'John Doe', age: 30 } - - expect(User.find(1)).toEqual(expected) - }) - - it('it will insert the record as is if the `beforeCreate` hook returns nothing', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(_record: User) { - // Do nothing. - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - - const expected = { $id: '1', id: 1, name: 'John Doe', age: 20 } - - expect(User.find(1)).toEqual(expected) - }) - - it('can cancel the insert by returning false from `beforeCreate` hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeCreate(record: User): boolean | void { - if (record.age === 20) { - return false - } - } - } - - createStore([{ model: User }]) - - await User.insert({ - data: [ - { id: 1, name: 'John Doe', age: 20 }, - { id: 2, name: 'Jane Doe', age: 24 } - ] - }) - - const users = User.all() - - expect(users.length).toBe(1) - expect(users[0].id).toBe(2) - }) - - it('can dispatch the `afterCreate` hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static afterCreate(record: User) { - expect(record).toBeInstanceOf(User) - expect(record.id).toBe(1) - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - }) -}) diff --git a/test/feature/hooks/Local_Select.spec.ts b/test/feature/hooks/Local_Select.spec.ts deleted file mode 100644 index 86faa9e4..00000000 --- a/test/feature/hooks/Local_Select.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Hooks – Local Select', () => { - it('can process beforeSelect hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - role!: string - - static fields() { - return { - id: this.attr(null), - role: this.attr('') - } - } - - static beforeSelect(records: any) { - return records.filter((record: User) => record.role === 'admin') - } - } - - createStore([{ model: User }]) - - await User.create({ - data: [ - { id: 1, role: 'admin' }, - { id: 2, role: 'admin' }, - { id: 3, role: 'user' } - ] - }) - - const users = User.query().get() - - expect(users.length).toBe(2) - expect(users[0].role).toBe('admin') - expect(users[1].role).toBe('admin') - }) -}) diff --git a/test/feature/hooks/Local_Update.spec.ts b/test/feature/hooks/Local_Update.spec.ts deleted file mode 100644 index ae861af4..00000000 --- a/test/feature/hooks/Local_Update.spec.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Hooks – Local Update', () => { - it('can dispatch the `beforeUpdate` hook that modifies the data being updated', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeUpdate(user: User) { - user.age = 30 - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - - await User.update({ id: 1, name: 'Jane Doe' }) - - const expected = { $id: '1', id: 1, name: 'Jane Doe', age: 30 } - - expect(User.find(1)).toEqual(expected) - }) - - it('it will update the record as is if the `beforeUpdate` hook returns nothing', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeUpdate(_user: User) { - // Do nothing. - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - - await User.update({ id: 1, name: 'Jane Doe', age: 30 }) - - const expected = { $id: '1', id: 1, name: 'Jane Doe', age: 30 } - - expect(User.find(1)).toEqual(expected) - }) - - it('can cancel the update by returning false from `beforeUpdate` hook', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static beforeUpdate(_user: User) { - return false - } - } - - createStore([{ model: User }]) - - await User.insert([ - { id: 1, name: 'John Doe', age: 20 }, - { id: 2, name: 'Jane Doe', age: 24 } - ]) - - await User.update({ id: 1, name: 'Johnny Doe' }) - - const users = User.all() - - expect(users.length).toBe(2) - expect(users[0].id).toBe(1) - expect(users[0].name).toBe('John Doe') - }) - - it('can dispatch the `afterUpdate` hook', async () => { - let hit = null - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null) - } - } - - static afterUpdate(user: User) { - hit = true - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe(1) - expect(user.age).toBe(30) - } - } - - createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe', age: 20 }) - - await User.update({ id: 1, age: 30 }) - - expect(hit).toBe(true) - }) -}) diff --git a/test/feature/inheritance/Inheritance.spec.ts b/test/feature/inheritance/Inheritance.spec.ts deleted file mode 100644 index 8c87cec5..00000000 --- a/test/feature/inheritance/Inheritance.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Inheritance', () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person, - CHILD: Child - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.string('') - } - } - - id!: any - name!: any - type!: string - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - job: this.attr('') - } - } - - job!: any - } - - class Child extends Person { - static entity = 'child' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields() - } - } - } - - it('should return right type string for given mode', () => { - expect(Model.getTypeKeyValueFromModel()).toBe(null) - expect(Person.getTypeKeyValueFromModel()).toBe('PERSON') - expect(Adult.getTypeKeyValueFromModel()).toBe('ADULT') - expect(Child.getTypeKeyValueFromModel()).toBe('CHILD') - }) - - it('sets right class for derived entity', async () => { - createStore([Person, Adult]) - - await Adult.insert({ id: 1, name: 'John Doe' }) - - expect(Adult.find(1)).toBeInstanceOf(Adult) - }) - - it('should inherit fields from root entity', async () => { - createStore([Person, Adult]) - - await Adult.insert({ id: 2, name: 'John Doe' }) - - const adult = Adult.find(2) as Adult - - expect(adult.id).toBe(2) - expect(adult.name).toBe('John Doe') - }) - - it('should have own fields only on derived entity', async () => { - createStore([Person, Adult]) - - await Person.insert({ id: 1, name: 'John' }) - - await Adult.insert({ id: 2, name: 'Jane', job: 'Software Engineer' }) - - const person = Person.find(1) as any - expect(person.job).toBe(undefined) - - const adult = Adult.find(2) as Adult - expect(adult.job).toBe('Software Engineer') - }) -}) - -describe('Feature - Inheritance - No Type Warning', () => { - class Person extends Model { - static entity = 'person' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.string('') - } - } - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - job: this.attr('') - } - } - } - - it('should warn user that no type is defined', () => { - const spy = jest.spyOn(global.console, 'warn').mockImplementation() - - createStore([Person, Adult]) - - expect(spy).toHaveBeenCalled() - }) -}) diff --git a/test/feature/inheritance/Inheritance_CRUD.spec.ts b/test/feature/inheritance/Inheritance_CRUD.spec.ts deleted file mode 100644 index 3ae829af..00000000 --- a/test/feature/inheritance/Inheritance_CRUD.spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature - Inheritance - CRUD', () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person, - CHILD: Child - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.string('') - } - } - - id!: any - name!: any - type!: string - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - job: this.attr('') - } - } - - job!: any - } - - class Child extends Person { - static entity = 'child' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields() - } - } - } - - it('can get mixed results when calling getter of root entity', async () => { - createStore([Person, Adult]) - - await Person.insert({ id: 1, name: 'John' }) - - await Adult.insert({ id: 2, name: 'Jane', job: 'Software Engineer' }) - - const people = Person.query() - .orderBy('id') - .get() - - expect(people.length).toBe(2) - - expect(people[0].name).toBe('John') - expect(people[0]).toBeInstanceOf(Person) - expect(people[0]).not.toBeInstanceOf(Adult) - - expect(people[1].name).toBe('Jane') - expect(people[1]).toBeInstanceOf(Adult) - }) - - it('can get only derived results when calling getter of derived entity', async () => { - createStore([Person, Adult]) - - await Person.insert({ id: 1, name: 'John' }) - - await Adult.insert({ id: 2, name: 'Jane', job: 'Software Engineer' }) - - const adults = Adult.all() - - expect(adults.length).toBe(1) - - expect(adults[0].name).toBe('Jane') - expect(adults[0]).toBeInstanceOf(Adult) - }) - - it('should clean only corresponding entities (and derived ones) when calling create', async () => { - createStore([Person, Adult, Child]) - - await Person.insert([ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' }, - { id: 3, name: 'D', type: 'CHILD' } - ]) - - expect(Person.all().length).toBe(3) - - await Adult.create({ id: 4, name: 'E' }) - - expect(Person.all().length).toBe(3) - - const adults = Adult.all() - - expect(adults.length).toBe(1) - expect(adults[0].id).toBe(4) - }) - - it('should update derived data without changing its class when calling base update', async () => { - createStore([Person, Adult]) - - await Person.create([ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ]) - - expect((Person.find(2) as Adult).name).toBe('B') - - await Person.update({ name: 'C' }, { where: 2 }) - - const adult = Person.find(2) as Adult - - expect(adult.name).toBe('C') - expect(adult).toBeInstanceOf(Adult) - }) - - it('should update data with a closure using instanceof', async () => { - createStore([Person, Adult]) - - await Person.create([ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ]) - - await Person.update( - { name: 'C' }, - { where: (record) => record instanceof Adult } - ) - - const adult = Person.find(2) as Adult - - expect(adult.name).toBe('C') - expect(adult).toBeInstanceOf(Adult) - }) - - it('should update or insert mixed data', async () => { - const store = createStore([Person, Adult]) - - await Person.create({ - id: 1, - name: 'John Doe', - job: 'Software Engineer', - type: 'ADULT' - }) - - await Person.insertOrUpdate([ - { id: 1, name: 'John Doe', job: 'Writer', type: 'ADULT' }, - { id: 2, name: 'Jane Doe', job: 'QA', type: 'ADULT' }, - { id: 3, name: 'Jane Doe', type: 'PERSON' } - ]) - - const expected = createState({ - person: { - 1: { $id: '1', id: 1, name: 'John Doe', job: 'Writer', type: 'ADULT' }, - 2: { $id: '2', id: 2, name: 'Jane Doe', job: 'QA', type: 'ADULT' }, - 3: { $id: '3', id: 3, name: 'Jane Doe', type: 'PERSON' } - }, - adult: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('should update only derived data when field is a derived entity field', async () => { - createStore([Person, Adult]) - - await Person.create([ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', job: 'Software Engineer', type: 'ADULT' } - ]) - - await Person.update( - { job: 'Writer' }, - { where: (record) => record instanceof Person } - ) - - const person = Person.find(1) as any - expect(person.job).toBe(undefined) - - const adult = Person.find(2) as Adult - expect(adult.job).toBe('Writer') - }) - - it('should delete record correctly when manipulating a derived entity', async () => { - createStore([Person, Adult]) - - await Person.create([ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ]) - - expect(Person.all().length).toBe(2) - - await Adult.delete(2) - - const people = Person.all() - - expect(people.length).toBe(1) - expect(people[0].id).toBe(1) - }) - - it('should delete only derived records when calling deleteAll on derived entity', async () => { - createStore([Person, Adult]) - - await Person.create([ - { id: 1, name: 'A', type: 'PERSON' }, - { id: 2, name: 'B', type: 'ADULT' } - ]) - - expect(Person.all().length).toBe(2) - - await Adult.deleteAll() - - const people = Person.all() - - expect(people.length).toBe(1) - expect(people[0].id).toBe(1) - }) -}) diff --git a/test/feature/inheritance/Inheritance_Discriminator.spec.ts b/test/feature/inheritance/Inheritance_Discriminator.spec.ts deleted file mode 100644 index 2b5f3ba2..00000000 --- a/test/feature/inheritance/Inheritance_Discriminator.spec.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature - Inheritance - Discriminator field', () => { - it('uses default `type` field when no typeKey is defined', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - PERSON: Person, - ADULT: Adult - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Adult extends Person { - static entity = 'adult' - static baseEntity = 'person' - } - - createStore([Person, Adult]) - - await Person.insert({ id: 1, name: 'John Doe', type: 'ADULT' }) - - expect(Adult.find(1)).toBeInstanceOf(Adult) - }) - - it('uses overwritten typeKey as discriminator when defined', async () => { - class Person extends Model { - static entity = 'person' - - static typeKey = 'the_key' - - static types() { - return { - PERSON: Person, - ADULT: Adult - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - the_key: this.attr('') - } - } - } - - class Adult extends Person { - static entity = 'adult' - static baseEntity = 'person' - } - - createStore([Person, Adult]) - - await Person.insert({ id: 1, name: 'John Doe', the_key: 'ADULT' }) - - expect(Adult.find(1)).toBeInstanceOf(Adult) - }) - - it('dispatches mixed data and hydrates with the right models', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - PERSON: Person, - ADULT: Adult, - CHILD: Child - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.attr('') - } - } - } - - class Child extends Person { - static entity = 'child' - static baseEntity = 'person' - } - - class Adult extends Person { - static entity = 'adult' - static baseEntity = 'person' - } - - createStore([Person, Adult, Child]) - - await Person.insert([ - { id: 1, name: 'John Doe', type: 'ADULT' }, - { id: 2, name: 'John Doe Jr', type: 'CHILD' }, - { id: 3, name: 'Doe', type: 'PERSON' }, - { id: 4, name: 'Jane Doe', type: 'ADULT' } - ]) - - const adults = Adult.all() - expect(adults.length).toBe(2) - expect(adults[0]).toBeInstanceOf(Adult) - - const children = Child.all() - expect(children.length).toBe(1) - expect(children[0]).toBeInstanceOf(Child) - - const persons = Person.all() - expect(persons.length).toBe(4) - }) - - it('should set the right discriminator value if inserted through derived Query', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - PERSON: Person, - ADULT: Adult - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.attr('') - } - } - - type!: any - } - - class Adult extends Person { - static entity = 'adult' - static baseEntity = 'person' - } - - createStore([Person, Adult]) - - // Inserting through "adult" model. - await Adult.create({ id: 1, name: 'John Doe' }) - - const adult = Adult.find(1) as Adult - expect(adult.type).toBe('ADULT') - }) - - it('should handle discriminator as Number', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - 0: Person, - 1: Adult - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.number(0) - } - } - - name!: any - type!: any - } - - class Adult extends Person { - static entity = 'adult' - static baseEntity = 'person' - } - - createStore([Person, Adult]) - - await Person.create([ - { id: 1, name: 'John Doe', type: 1 }, - { id: 2, name: 'Person Doe', type: 0 } - ]) - - const person = Person.find(2) as Person - expect(person.name).toBe('Person Doe') - expect(person.type).toBe(0) - - const adult = Adult.find(1) as Adult - expect(adult.name).toBe('John Doe') - }) -}) diff --git a/test/feature/inheritance/Inheritance_RelationInstantiation.spec.ts b/test/feature/inheritance/Inheritance_RelationInstantiation.spec.ts deleted file mode 100644 index ea3d2ac5..00000000 --- a/test/feature/inheritance/Inheritance_RelationInstantiation.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature - Inheritance - Relation instantiation', () => { - it('should choose the appropriate STI model class when instantiating a belongsTo relation', () => { - class Employee extends Model { - static entity = 'employee' - - static types() { - return { - boss: Boss - } - } - - static fields() { - return { - id: this.attr(null), - name: this.string(''), - type: this.string('') - } - } - } - - class Boss extends Employee { - static entity = 'boss' - static baseEntity = 'employee' - } - - class Firm extends Model { - static entity = 'firm' - - static fields() { - return { - id: this.attr(null), - owner_id: this.attr(null), - name: this.string(''), - employees: this.hasMany(Employee, 'firm_id'), - owner: this.belongsTo(Employee, 'owner_id') - } - } - - owner!: any - } - - createStore([Employee, Boss, Firm]) - - const firm = new Firm({ - id: 1, - name: 'VUEX ORM Ltd.', - owner: { id: 'Jax', type: 'boss' } - }) - - expect(firm.owner).toBeInstanceOf(Boss) - }) - - it('should choose the appropriate STI model class when instantiating hasMany relations', () => { - class Employee extends Model { - static entity = 'employee' - - static types() { - return { - HR: HumanResources, - CEO: ChiefExecutiveOfficer - } - } - - static fields() { - return { - id: this.attr(null), - firm_id: this.attr(null), - name: this.string(''), - type: this.string('') - } - } - - id!: any - } - - class HumanResources extends Employee { - static entity = 'hr' - static baseEntity = 'employee' - } - - class ChiefExecutiveOfficer extends Employee { - static entity = 'ceo' - static baseEntity = 'employee' - } - - class Firm extends Model { - static entity = 'firm' - - static fields() { - return { - id: this.attr(null), - name: this.string(''), - employees: this.hasMany(Employee, 'firm_id') - } - } - - employees!: Employee[] - } - - createStore([Employee, HumanResources, ChiefExecutiveOfficer, Firm]) - - const firm = new Firm({ - id: 1, - name: 'VUEX ORM Ltd.', - employees: [ - { id: 1, name: 'John', type: 'HR' }, - { id: 2, name: 'Jane', type: 'CEO' } - ] - }) - - expect(firm.employees.find(({ id }) => id === 1)).toBeInstanceOf( - HumanResources - ) - expect(firm.employees.find(({ id }) => id === 2)).toBeInstanceOf( - ChiefExecutiveOfficer - ) - }) - - it('should choose the appropriate STI model class when instantiating a morphTo relation', () => { - class User extends Model { - static entity = 'user' - - static types() { - return { - admin: Admin, - user: User - } - } - - static fields() { - return { - id: this.attr(null), - name: this.string(''), - image: this.morphOne(Image, 'imageable_id', 'imageable_type') - } - } - } - - class Admin extends User { - static entity = 'admin' - static baseEntity = 'user' - } - - class Image extends Model { - static entity = 'image' - - static fields() { - return { - id: this.attr(null), - url: this.attr(''), - imageable_id: this.attr(null), - imageable_type: this.attr(null), - imageable: this.morphTo('imageable_id', 'imageable_type') - } - } - - imageable!: User | Admin - } - - createStore([User, Admin, Image]) - - const image = new Image({ - id: 1, - url: 'myPng.gif', - imageable_type: 'user', - imageable: { id: 1, name: 'Jane', type: 'admin' } - }) - - expect(image.imageable).toBeInstanceOf(Admin) - }) -}) diff --git a/test/feature/inheritance/Inheritance_Relations.spec.ts b/test/feature/inheritance/Inheritance_Relations.spec.ts deleted file mode 100644 index d3e752f5..00000000 --- a/test/feature/inheritance/Inheritance_Relations.spec.ts +++ /dev/null @@ -1,433 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature - Inheritance - Relations', () => { - it('can fetch a related data defined on derived entity', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.string('') - } - } - - id!: any - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - jobs: this.hasMany(Job, 'adult_id') - } - } - - jobs!: Job[] - } - - class Job extends Model { - static entity = 'jobs' - - static fields() { - return { - id: this.attr(null), - adult_id: this.attr(null), - title: this.attr(''), - adult: this.belongsTo(Adult, 'adult_id') - } - } - } - - createStore([Person, Adult, Job]) - - await Job.insert({ id: 1, title: 'Software Engineer', adult_id: 2 }) - - await Person.insert([ - { id: 1, name: 'John', type: 'PERSON' }, - { id: 2, name: 'Jane', type: 'ADULT' } - ]) - - const adult = Adult.query() - .with('jobs') - .all() - - expect(adult.length).toBe(1) - expect(adult[0].id).toBe(2) - expect(adult[0].jobs.length).toBe(1) - }) - - it('can fetch a related data which is a derived entity', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.string('') - } - } - - id!: any - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - jobs: this.hasMany(Job, 'adult_id') - } - } - - jobs!: Job[] - } - - class Job extends Model { - static entity = 'jobs' - - static fields() { - return { - id: this.attr(null), - adult_id: this.attr(null), - title: this.attr(''), - adult: this.belongsTo(Adult, 'adult_id') - } - } - - adult!: Adult | null - } - - createStore([Person, Adult, Job]) - - await Job.insert({ id: 1, title: 'Software Engineer', adult_id: 2 }) - - await Person.insert([ - { id: 1, name: 'John', type: 'PERSON' }, - { id: 2, name: 'Jane', type: 'ADULT' } - ]) - - const job = Job.query() - .with('adult') - .all() - - expect(job.length).toBe(1) - expect(job[0].adult).not.toBe(null) - expect((job[0].adult as Adult).id).toBe(2) - }) - - it('can fetch mixed data when related to a base entity', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - role_id: this.attr(null), - type: this.string('') - } - } - - id!: any - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields() - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null), - roleName: this.attr(''), - people: this.hasMany(Person, 'role_id') - } - } - - id!: any - people!: Person[] - } - - createStore([Person, Adult, Role]) - - await Role.insert([ - { id: 1, roleName: 'Role 1' }, - { id: 2, roleName: 'Role 2' } - ]) - - await Person.insert([ - { id: 1, name: 'John', type: 'PERSON', role_id: 1 }, - { id: 2, name: 'Jane', type: 'ADULT', role_id: 1 } - ]) - - const roles = Role.query() - .with('people') - .all() - expect(roles.length).toBe(2) - - // Checking people have been fetched. - const role = roles.filter((r) => r.id === 1)[0] - expect(role.people.length).toBe(2) - - // Checking that there is one Adult in the people array. - const adults = role.people.filter((p) => p instanceof Adult) - expect(adults.length).toBe(1) - expect(adults[0].id).toBe(2) - }) - - it('should keep right type when fetching related data from base entity', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - role_id: this.attr(null), - type: this.string(''), - role: this.belongsTo(Role, 'role_id') - } - } - - id!: any - role!: Role | null - } - - class Adult extends Person { - static entity = 'adult' - static baseEntity = 'person' - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null), - roleName: this.attr(''), - people: this.hasMany(Person, 'role_id') - } - } - - id!: any - } - - createStore([Person, Adult, Role]) - - await Role.insert([ - { id: 1, roleName: 'Role 1' }, - { id: 2, roleName: 'Role 2' } - ]) - - await Person.insert([ - { id: 1, name: 'John', type: 'PERSON', role_id: 1 }, - { id: 2, name: 'Jane', type: 'ADULT', role_id: 2 } - ]) - - // Reverse check: getting all people and their associated role. - const people = Person.query() - .with('role') - .all() - expect(people.length).toBe(2) - - const firstPerson = people.filter((p) => p.id === 1)[0] - expect(firstPerson).toBeInstanceOf(Person) - expect((firstPerson.role as Role).id).toBe(1) - - const secondPerson = people.filter((p) => p.id === 2)[0] - expect(secondPerson).toBeInstanceOf(Adult) - expect((secondPerson.role as Role).id).toBe(2) - }) - - it('should allow definition of relationship to derived / base entities', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person, - CHILD: Child - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - type: this.string('') - } - } - - id!: any - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - child: this.belongsTo(Child, 'child_id'), - child_id: this.attr(null) - } - } - - child!: Child - } - - class Child extends Person { - static entity = 'child' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - parents: this.hasMany(Adult, 'child_id') - } - } - - parents!: Adult[] - } - - createStore([Person, Adult, Child]) - - await Person.insert([ - { id: 1, name: 'John', type: 'ADULT', child_id: 3 }, - { id: 2, name: 'Jane', type: 'ADULT', child_id: 3 }, - { id: 3, name: 'Jane', type: 'CHILD' } - ]) - - const adults = Adult.query() - .with('child') - .all() - expect(adults.length).toBe(2) - expect(adults[0].child.id).toBe(3) - expect(adults[1].child.id).toBe(3) - - const children = Child.query() - .with('parents') - .all() - expect(children.length).toBe(1) - expect(children[0].parents.length).toBe(2) - }) - - it('can fetch a related data defined on derived entity when calling root getter', async () => { - class Person extends Model { - static entity = 'person' - - static types() { - return { - ADULT: Adult, - PERSON: Person - } - } - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - id!: any - } - - class Adult extends Person { - static entity = 'adult' - - static baseEntity = 'person' - - static fields() { - return { - ...super.fields(), - jobs: this.hasMany(Job, 'adult_id') - } - } - - jobs!: Job[] - } - - class Job extends Model { - static entity = 'jobs' - - static fields() { - return { - id: this.attr(null), - adult_id: this.attr(null), - title: this.attr(''), - adult: this.belongsTo(Adult, 'adult_id') - } - } - } - - createStore([Person, Adult, Job]) - - await Job.insert({ id: 1, title: 'Software Engineer', adult_id: 2 }) - - await Person.insert([ - { id: 1, name: 'John', type: 'PERSON' }, - { id: 2, name: 'Jane', type: 'ADULT' } - ]) - - const persons = Person.query() - .with(['jobs', 'dummy']) - .all() - expect(persons.length).toBe(2) - - const adult = persons.filter((p) => p.id === 2)[0] as Adult - expect(adult.jobs.length).toBe(1) - }) -}) diff --git a/test/feature/models/All.spec.ts b/test/feature/models/All.spec.ts deleted file mode 100644 index b6b4859e..00000000 --- a/test/feature/models/All.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Models – All', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - it('can fetch all records via static method', async () => { - createStore([{ model: User }]) - - await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const users = User.all() - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - }) - - it('can fetch all records via instance method', async () => { - createStore([{ model: User }]) - - await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const user = new User() - - const users = user.$all() - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - }) -}) diff --git a/test/feature/models/Create.spec.ts b/test/feature/models/Create.spec.ts deleted file mode 100644 index 6216af64..00000000 --- a/test/feature/models/Create.spec.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' -import Utils from '@/support/Utils' - -describe('Feature – Models – Create', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - it('can create a record via static method', async () => { - const store = createStore([{ model: User }]) - - await User.create({ id: 1, name: 'John Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a list of record via static method', async () => { - const store = createStore([{ model: User }]) - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns created records via static method', async () => { - createStore([{ model: User }]) - - const data = await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - expect(data.users.length).toBe(2) - expect(data.users[0]).toBeInstanceOf(User) - }) - - it('can create a record via instance method', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await user.$create({ id: 1, name: 'John Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a list of record via instance method', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await user.$create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns created records via instance method', async () => { - createStore([{ model: User }]) - - const user = new User() - - const data = await user.$create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - expect(data.users.length).toBe(2) - expect(data.users[0]).toBeInstanceOf(User) - }) - - it('can create a record via static method while preserving input data', async () => { - class Author extends Model { - static entity = 'authors' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.hasMany(Post, 'author_id'), - comments: this.hasMany(Comment, 'author_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - author_id: this.attr(null), - comments_ids: this.attr([]), - title: this.attr(''), - author: this.belongsTo(Author, 'author_id'), - comments: this.hasManyBy(Comment, 'comments_ids') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - author_id: this.attr(null), - body: this.attr(''), - author: this.belongsTo(Author, 'author_id') - } - } - } - - const store = createStore([ - { model: Author }, - { model: Post }, - { model: Comment } - ]) - - const data = { - $id: '1', - id: 1, - name: 'John Doe', - posts: [ - { - $id: '1', - id: 1, - author_id: 1, - title: 'Post Title', - comments: [ - { - $id: '1', - id: 1, - author: { - $id: '2', - id: 2, - name: 'Fred Fenster' - }, - body: 'Comment Body' - }, - { - $id: '2', - id: 2, - author: { - $id: '3', - id: 3, - name: 'Alejandro Gillick' - }, - body: 'Comment Body' - } - ] - } - ] - } - - const dataBefore = Utils.cloneDeep(data) - - expect(data).toEqual(dataBefore) - - await Author.create(data) - - const expected = createState({ - authors: { - 1: { - $id: '1', - id: 1, - name: 'John Doe', - posts: [], - comments: [] - }, - 2: { - $id: '2', - id: 2, - name: 'Fred Fenster', - posts: [], - comments: [] - }, - 3: { - $id: '3', - id: 3, - name: 'Alejandro Gillick', - posts: [], - comments: [] - } - }, - posts: { - 1: { - $id: '1', - id: 1, - author_id: 1, - comments_ids: [1, 2], - title: 'Post Title', - comments: [], - author: null - } - }, - comments: { - 1: { - $id: '1', - id: 1, - author_id: 2, - body: 'Comment Body', - author: null - }, - 2: { - $id: '2', - id: 2, - author_id: 3, - body: 'Comment Body', - author: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - expect(data).toEqual(dataBefore) - }) -}) diff --git a/test/feature/models/Find.spec.ts b/test/feature/models/Find.spec.ts deleted file mode 100644 index 653d3ddc..00000000 --- a/test/feature/models/Find.spec.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Models – Find', () => { - describe('#static', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - }) - - it('can fetch a record', async () => { - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const user = User.find(1) - - const expected = { $id: '1', id: 1, name: 'John Doe' } - - expect(user).toEqual(expected) - expect(user).toBeInstanceOf(User) - }) - - it('can fetch array of records', async () => { - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Smith Doe' } - ] - }) - - const users = User.findIn([1, 3]) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '3', id: 3, name: 'Smith Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - }) - - it('returns empty collection when passing a primitive value', async () => { - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - const user = User.findIn(1 as any) - - expect(user).toEqual([]) - }) - - it('throws error when passing non-primitive value to `find`', async () => { - createStore([{ model: User }]) - - expect(() => User.find([1, 1])).toThrowError('[Vuex ORM]') - }) - }) - - describe('#instance', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - }) - - it('can fetch a record', async () => { - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const u = new User() - - const user = u.$find(1) - - const expected = { $id: '1', id: 1, name: 'John Doe' } - - expect(user).toEqual(expected) - expect(user).toBeInstanceOf(User) - }) - - it('can fetch array of records', async () => { - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Smith Doe' } - ] - }) - - const u = new User() - - const users = u.$findIn([1, 3]) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '3', id: 3, name: 'Smith Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - }) - - it('returns empty collection when passing a primitive value', async () => { - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - const u = new User() - - const user = u.$findIn(1 as any) - - expect(user).toEqual([]) - }) - - it('throws error when passing non-primitive value to `find`', async () => { - const u = new User() - - expect(() => u.$find([1, 1])).toThrowError('[Vuex ORM]') - }) - }) - - describe('with composite primary key', () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['workspace_id', 'id'] - - static fields() { - return { - workspace_id: this.attr(null), - id: this.attr(null), - name: this.attr('') - } - } - } - - beforeEach(() => { - createStore([{ model: User }]) - }) - - describe('#static', () => { - it('can fetch a record', async () => { - await User.insert({ - data: [ - { id: 1, workspace_id: 1, name: 'John Doe' }, - { id: 2, workspace_id: 1, name: 'Jane Doe' } - ] - }) - - const user = User.find([1, 1]) - - const expected = { - $id: '[1,1]', - id: 1, - workspace_id: 1, - name: 'John Doe' - } - - expect(user).toEqual(expected) - expect(user).toBeInstanceOf(User) - }) - - it('can fetch array of records', async () => { - await User.insert({ - data: [ - { id: 1, workspace_id: 1, name: 'John Doe' }, - { id: 2, workspace_id: 1, name: 'Jane Doe' }, - { id: 3, workspace_id: 1, name: 'Smith Doe' } - ] - }) - - const users = User.findIn([ - [1, 1], - [1, 3] - ]) - - const expected = [ - { $id: '[1,1]', id: 1, workspace_id: 1, name: 'John Doe' }, - { $id: '[1,3]', id: 3, workspace_id: 1, name: 'Smith Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - }) - - it('throws error when passing value other than an array to `find`', async () => { - expect(() => User.find(1)).toThrowError('[Vuex ORM]') - }) - - it('throws error when passing value other than a nested array to `findIn`', async () => { - expect(() => User.findIn([1, 1])).toThrowError('[Vuex ORM]') - }) - }) - - describe('#instance', () => { - it('can fetch a record', async () => { - await User.insert({ - data: [ - { id: 1, workspace_id: 1, name: 'John Doe' }, - { id: 2, workspace_id: 1, name: 'Jane Doe' } - ] - }) - - const u = new User() - - const user = u.$find([1, 1]) - - const expected = { - $id: '[1,1]', - id: 1, - workspace_id: 1, - name: 'John Doe' - } - - expect(user).toEqual(expected) - expect(user).toBeInstanceOf(User) - }) - - it('can fetch array of records', async () => { - await User.insert({ - data: [ - { id: 1, workspace_id: 1, name: 'John Doe' }, - { id: 2, workspace_id: 1, name: 'Jane Doe' }, - { id: 3, workspace_id: 1, name: 'Smith Doe' } - ] - }) - - const u = new User() - - const users = u.$findIn([ - [1, 1], - [1, 3] - ]) - - const expected = [ - { $id: '[1,1]', id: 1, workspace_id: 1, name: 'John Doe' }, - { $id: '[1,3]', id: 3, workspace_id: 1, name: 'Smith Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - expect(users[1]).toBeInstanceOf(User) - }) - - it('throws error when passing value other than an array to `find`', async () => { - const u = new User() - - expect(() => u.$find(1)).toThrowError('[Vuex ORM]') - }) - - it('throws error when passing value other than a nested array to `findIn`', async () => { - const u = new User() - - expect(() => u.$findIn([1, 1])).toThrowError('[Vuex ORM]') - }) - }) - }) -}) diff --git a/test/feature/models/Insert.spec.ts b/test/feature/models/Insert.spec.ts deleted file mode 100644 index 05be819b..00000000 --- a/test/feature/models/Insert.spec.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' -import Utils from '@/support/Utils' - -describe('Feature – Models – Insert', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - it('can insert a record via static method', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ id: 1, name: 'John Doe' }) - - await User.insert({ id: 2, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert a list of records via static method', async () => { - const store = createStore([{ model: User }]) - - await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns inserted records via static method', async () => { - createStore([{ model: User }]) - - const data = await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - expect(data.users.length).toBe(2) - expect(data.users[0]).toBeInstanceOf(User) - }) - - it('can insert a record via instance method', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await user.$insert({ id: 1, name: 'John Doe' }) - - await user.$insert({ id: 2, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert a list of records via instance method', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await user.$insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns inserted records via instance method', async () => { - createStore([{ model: User }]) - - const user = new User() - - const data = await user.$insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - expect(data.users.length).toBe(2) - expect(data.users[0]).toBeInstanceOf(User) - }) - - it('can insert a record via static method while preserving input data', async () => { - class Author extends Model { - static entity = 'authors' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.hasMany(Post, 'author_id'), - comments: this.hasMany(Comment, 'author_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - author_id: this.attr(null), - comments_ids: this.attr([]), - title: this.attr(''), - author: this.belongsTo(Author, 'author_id'), - comments: this.hasManyBy(Comment, 'comments_ids') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - author_id: this.attr(null), - body: this.attr(''), - author: this.belongsTo(Author, 'author_id') - } - } - } - - const store = createStore([ - { model: Author }, - { model: Post }, - { model: Comment } - ]) - - const data = { - $id: '1', - id: 1, - name: 'John Doe', - posts: [ - { - $id: '1', - id: 1, - author_id: 1, - title: 'Post Title', - comments: [ - { - $id: '1', - id: 1, - author: { - $id: '2', - id: 2, - name: 'Fred Fenster' - }, - body: 'Comment Body' - }, - { - $id: '2', - id: 2, - author: { - $id: '3', - id: 3, - name: 'Alejandro Gillick' - }, - body: 'Comment Body' - } - ] - } - ] - } - - const dataBefore = Utils.cloneDeep(data) - - expect(data).toEqual(dataBefore) - - await Author.insert(data) - - const expected = createState({ - authors: { - 1: { - $id: '1', - id: 1, - name: 'John Doe', - posts: [], - comments: [] - }, - 2: { - $id: '2', - id: 2, - name: 'Fred Fenster', - posts: [], - comments: [] - }, - 3: { - $id: '3', - id: 3, - name: 'Alejandro Gillick', - posts: [], - comments: [] - } - }, - posts: { - 1: { - $id: '1', - id: 1, - author_id: 1, - comments_ids: [1, 2], - title: 'Post Title', - comments: [], - author: null - } - }, - comments: { - 1: { - $id: '1', - id: 1, - author_id: 2, - body: 'Comment Body', - author: null - }, - 2: { - $id: '2', - id: 2, - author_id: 3, - body: 'Comment Body', - author: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - expect(data).toEqual(dataBefore) - }) -}) diff --git a/test/feature/models/InsertOrUpdate.spec.ts b/test/feature/models/InsertOrUpdate.spec.ts deleted file mode 100644 index 09b58e49..00000000 --- a/test/feature/models/InsertOrUpdate.spec.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' -import Utils from '@/support/Utils' - -describe('Feature – Models – Insert Or Update', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - it('can insert or update records via static method', async () => { - const store = createStore([{ model: User }]) - - await User.insert([{ id: 1, name: 'John Doe' }]) - - await User.insertOrUpdate([ - { id: 1, name: 'Jane Doe' }, - { id: 2, name: 'Johnny Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' }, - 2: { $id: '2', id: 2, name: 'Johnny Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert or update records via instance method', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await User.insert([{ id: 1, name: 'John Doe' }]) - - await user.$insertOrUpdate([ - { id: 1, name: 'Jane Doe' }, - { id: 2, name: 'Johnny Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' }, - 2: { $id: '2', id: 2, name: 'Johnny Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert or update records via static method while preserving input data', async () => { - class Author extends Model { - static entity = 'authors' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.hasMany(Post, 'author_id'), - comments: this.hasMany(Comment, 'author_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - author_id: this.attr(null), - comments_ids: this.attr([]), - title: this.attr(''), - author: this.belongsTo(Author, 'author_id'), - comments: this.hasManyBy(Comment, 'comments_ids') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - author_id: this.attr(null), - body: this.attr(''), - author: this.belongsTo(Author, 'author_id') - } - } - } - - const store = createStore([ - { model: Author }, - { model: Post }, - { model: Comment } - ]) - - const data = { - $id: '1', - id: 1, - name: 'John Doe', - posts: [ - { - $id: '1', - id: 1, - author_id: 1, - title: 'Post Title', - comments: [ - { - $id: '1', - id: 1, - author: { - $id: '2', - id: 2, - name: 'Fred Fenster' - }, - body: 'Comment Body' - }, - { - $id: '2', - id: 2, - author: { - $id: '3', - id: 3, - name: 'Alejandro Gillick' - }, - body: 'Comment Body' - } - ] - } - ] - } - - const dataBefore = Utils.cloneDeep(data) - - expect(data).toEqual(dataBefore) - - await Author.insertOrUpdate({ - data - }) - - const expected = createState({ - authors: { - 1: { - $id: '1', - id: 1, - name: 'John Doe', - posts: [], - comments: [] - }, - 2: { - $id: '2', - id: 2, - name: 'Fred Fenster', - posts: [], - comments: [] - }, - 3: { - $id: '3', - id: 3, - name: 'Alejandro Gillick', - posts: [], - comments: [] - } - }, - posts: { - 1: { - $id: '1', - id: 1, - author_id: 1, - comments_ids: [1, 2], - title: 'Post Title', - comments: [], - author: null - } - }, - comments: { - 1: { - $id: '1', - id: 1, - author_id: 2, - body: 'Comment Body', - author: null - }, - 2: { - $id: '2', - id: 2, - author_id: 3, - body: 'Comment Body', - author: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - expect(data).toEqual(dataBefore) - }) -}) diff --git a/test/feature/models/Query.spec.ts b/test/feature/models/Query.spec.ts deleted file mode 100644 index a66d5691..00000000 --- a/test/feature/models/Query.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Models – Query', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - it('can begin query chain via static method', async () => { - createStore([{ model: User }]) - - await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const users = User.query().get() - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(users).toEqual(expected) - expect(users[0]).toBeInstanceOf(User) - }) - - it('can begin query chain via instance method', async () => { - createStore([{ model: User }]) - - await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const u = new User() - - const user = u.$query().find(1) - - const expected = { $id: '1', id: 1, name: 'John Doe' } - - expect(user).toEqual(expected) - expect(user).toBeInstanceOf(User) - }) -}) diff --git a/test/feature/models/Save.spec.ts b/test/feature/models/Save.spec.ts deleted file mode 100644 index d4bd8ced..00000000 --- a/test/feature/models/Save.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Models – Save', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @BelongsTo(User, 'user_id') - user!: User - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id') - } - } - } - - it('can save a new record', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - user.id = 1 - user.name = 'John Doe' - await user.$save() - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe', posts: [] } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can save a new record and ignore references', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - const user = new User() - user.id = 1 - user.name = 'John Doe' - await user.$save() - - const post = new Post() - post.id = 1 - post.user_id = user.id - post.user = user - await post.$save() - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe', posts: [] } - }, - posts: { - 1: { $id: '1', id: 1, user_id: 1, user: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/models/Update.spec.ts b/test/feature/models/Update.spec.ts deleted file mode 100644 index f1f7f13a..00000000 --- a/test/feature/models/Update.spec.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Models – Update', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - describe('#static', () => { - it('can update a record by passing in the id', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - await User.update({ id: 1, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can update a record by passing where value', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - await User.update({ - where: 1, - data: { name: 'Jane Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can update records by passing where closure', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Jane Doe' } - ] - }) - - await User.update({ - where(record) { - return record.name === 'Jane Doe' - }, - - data: { name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'John Doe' }, - 3: { $id: '3', id: 3, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can update a record by passing data closure', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' }, - { id: 3, name: 'Jane Doe' } - ] - }) - - await User.update({ - where: 1, - - data(record: User) { - record.name = 'Jane Doe' - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' }, - 3: { $id: '3', id: 3, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - }) - - describe('#instance', () => { - it('can update a record', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - await user.$update({ id: 1, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can omit id when updating', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - const user = store.getters['entities/users/find'](1) - - await user.$update({ name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can omit id when updating where model has a composite primary key', async () => { - class Role extends Model { - static entity = 'roles' - - static primaryKey = ['key_1', 'key_2'] - - static fields() { - return { - key_1: this.uid(), - key_2: this.uid(), - name: this.attr('') - } - } - } - - const store = createStore([{ model: Role }]) - - await Role.insert({ key_1: 1, key_2: 2, name: 'John Doe' }) - - const user = Role.find([1, 2]) as Role - - await user.$update({ name: 'Jane Doe' }) - - const expected = createState({ - roles: { - '[1,2]': { $id: '[1,2]', key_1: 1, key_2: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can update a record passing array of records', async () => { - const store = createStore([{ model: User }]) - - await User.insert({ - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const user = store.getters['entities/users/find'](1) - - await user.$update([ - { id: 1, name: 'Jane Doe' }, - { id: 2, name: 'Johnny Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' }, - 2: { $id: '2', id: 2, name: 'Johnny Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can update a record by passing where value', async () => { - const store = createStore([{ model: User }]) - - const user = new User() - - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - await user.$update({ name: 'Jane Doe' }, { where: 1 }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - }) -}) diff --git a/test/feature/models/static_inserts_create.spec.ts b/test/feature/models/static_inserts_create.spec.ts deleted file mode 100644 index 1e947c6b..00000000 --- a/test/feature/models/static_inserts_create.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - createStore, - createState, - serializeCollection -} from 'test/support/Helpers' -import { Model } from '@/index' - -describe('feature/models/static_inserts_create', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - it('can create a record', async () => { - const store = createStore([User]) - - await User.create({ id: 1, name: 'John Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create multiple records', async () => { - const store = createStore([User]) - - await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('replaces any existing records', async () => { - const store = createStore([User]) - - await User.create({ id: 1, name: 'John Doe' }) - await User.create({ id: 2, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns newly created data', async () => { - createStore([User]) - - const result = await User.create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(result.users.length).toBe(2) - expect(result.users[0]).toBeInstanceOf(User) - expect(result.users[1]).toBeInstanceOf(User) - expect(serializeCollection(result.users)).toEqual(expected) - }) - - // Test for deprecated payload API syntax. - it('accepts the "payload" object as an argument', async () => { - const store = createStore([User]) - - await User.create({ - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/models/static_inserts_insert.spec.ts b/test/feature/models/static_inserts_insert.spec.ts deleted file mode 100644 index 90e7ee0a..00000000 --- a/test/feature/models/static_inserts_insert.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - createStore, - createState, - serializeCollection -} from 'test/support/Helpers' -import { Model } from '@/index' - -describe('feature/models/static_inserts_insert', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - it('can insert a record', async () => { - const store = createStore([User]) - - await User.insert({ id: 1, name: 'John Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert multiple records', async () => { - const store = createStore([User]) - - await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('keeps existing records', async () => { - const store = createStore([User]) - - await User.insert({ id: 1, name: 'John Doe' }) - await User.insert({ id: 2, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns newly created data', async () => { - createStore([User]) - - const result = await User.insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(result.users.length).toBe(2) - expect(result.users[0]).toBeInstanceOf(User) - expect(result.users[1]).toBeInstanceOf(User) - expect(serializeCollection(result.users)).toEqual(expected) - }) - - // Test for deprecated payload API syntax. - it('accepts the "payload" object as an argument', async () => { - const store = createStore([User]) - - await User.insert({ - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/models/static_inserts_new.spec.ts b/test/feature/models/static_inserts_new.spec.ts deleted file mode 100644 index 1270857d..00000000 --- a/test/feature/models/static_inserts_new.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Uid from '@/support/Uid' -import { Model } from '@/index' - -describe('feature/query/inserts_new', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.uid(), - name: this.attr('John Doe') - } - } - - id!: string - name!: string - } - - beforeEach(() => { - Uid.reset() - }) - - it('can create new data with all fields filled by default values', async () => { - const store = createStore([User]) - - await User.new() - await User.new() - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' }, - $uid2: { $id: '$uid2', id: '$uid2', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns a newly created model', async () => { - createStore([User]) - - const user = await User.new() - - expect(user).toBeInstanceOf(User) - expect(user.$id).toBe('$uid1') - expect(user.id).toBe('$uid1') - expect(user.name).toBe('John Doe') - }) -}) diff --git a/test/feature/modules/SubGetters_DirectEntityCall.spec.ts b/test/feature/modules/SubGetters_DirectEntityCall.spec.ts deleted file mode 100644 index 0e76f92e..00000000 --- a/test/feature/modules/SubGetters_DirectEntityCall.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Sub Getters – Direct Entity Call', () => { - it('can directly call entity to fetch the repo instance', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('JD') - } - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const users = store.getters['entities/users/query']().get() - - expect(users.length).toBe(1) - expect(users[0]).toBeInstanceOf(User) - }) -}) diff --git a/test/feature/modules/api_inserts_create.spec.ts b/test/feature/modules/api_inserts_create.spec.ts deleted file mode 100644 index fd9ea8e8..00000000 --- a/test/feature/modules/api_inserts_create.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - createStore, - createState, - serializeCollection -} from 'test/support/Helpers' -import { Model } from '@/index' - -describe('feature/modules/api_inserts_create', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - it('can create a record', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create multiple records', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('replaces any existing records', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - await store.dispatch('entities/users/create', { - data: { id: 2, name: 'Jane Doe' } - }) - - const expected = createState({ - users: { - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns newly created data', async () => { - const store = createStore([User]) - - const result = await store.dispatch('entities/users/create', { - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(result.users.length).toBe(2) - expect(result.users[0]).toBeInstanceOf(User) - expect(result.users[1]).toBeInstanceOf(User) - expect(serializeCollection(result.users)).toEqual(expected) - }) -}) diff --git a/test/feature/modules/api_inserts_insert.spec.ts b/test/feature/modules/api_inserts_insert.spec.ts deleted file mode 100644 index 06645906..00000000 --- a/test/feature/modules/api_inserts_insert.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - createStore, - createState, - serializeCollection -} from 'test/support/Helpers' -import { Model } from '@/index' - -describe('feature/modules/inserts_insert', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - it('can insert a record', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/insert', { - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert multiple records', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/insert', { - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('keeps existing records', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/insert', { - data: { id: 1, name: 'John Doe' } - }) - await store.dispatch('entities/users/insert', { - data: { id: 2, name: 'Jane Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns newly created data', async () => { - const store = createStore([User]) - - const result = await store.dispatch('entities/users/insert', { - data: [ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ] - }) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(result.users.length).toBe(2) - expect(result.users[0]).toBeInstanceOf(User) - expect(result.users[1]).toBeInstanceOf(User) - expect(serializeCollection(result.users)).toEqual(expected) - }) -}) diff --git a/test/feature/modules/api_inserts_new.spec.ts b/test/feature/modules/api_inserts_new.spec.ts deleted file mode 100644 index fe91042a..00000000 --- a/test/feature/modules/api_inserts_new.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Uid from '@/support/Uid' -import { Model } from '@/index' - -describe('feature/modules/api_inserts_new', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.uid(), - name: this.attr('John Doe') - } - } - - id!: string - name!: string - } - - beforeEach(() => { - Uid.reset() - }) - - it('can create new data with all fields filled by default values', async () => { - const store = createStore([User]) - - await store.dispatch('entities/users/new') - await store.dispatch('entities/users/new') - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' }, - $uid2: { $id: '$uid2', id: '$uid2', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns a newly created model', async () => { - const store = createStore([User]) - - const user = await store.dispatch('entities/users/new') - - expect(user).toBeInstanceOf(User) - expect(user.$id).toBe('$uid1') - expect(user.id).toBe('$uid1') - expect(user.name).toBe('John Doe') - }) -}) diff --git a/test/feature/modules/modules.spec.ts b/test/feature/modules/modules.spec.ts deleted file mode 100644 index 85607d9d..00000000 --- a/test/feature/modules/modules.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Vue from 'vue' -import Vuex, { Module } from 'vuex' -import VuexORM, { Database, Model } from '@/index' - -describe('feature/modules/modules', () => { - Vue.use(Vuex) - - it('can install a custom module along with the model', async () => { - class User extends Model { - static entity = 'users' - } - - const users: Module<{ count: number }, any> = { - state: () => ({ - count: 1 - }), - getters: { - double: (state) => state.count * 2 - }, - actions: { - increment({ commit }) { - commit('increment') - } - }, - mutations: { - increment(state) { - state.count++ - } - } - } - - const database = new Database() - - database.register(User, users) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(store.state.entities.users.count).toBe(1) - expect(store.getters['entities/users/double']).toBe(2) - - await store.dispatch('entities/users/increment') - - expect(store.state.entities.users.count).toBe(2) - expect(store.getters['entities/users/double']).toBe(4) - }) -}) diff --git a/test/feature/plugin/Plugin.spec.ts b/test/feature/plugin/Plugin.spec.ts deleted file mode 100644 index 12bb6be3..00000000 --- a/test/feature/plugin/Plugin.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import Vue from 'vue' -import Vuex from 'vuex' -import VuexORM from '@/index' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Plugin', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - const database = new VuexORM.Database() - - database.register(User, {}) - - Vue.use(Vuex) - - it('add additional feature to the Model', async () => { - const plugin = { - install(components: any) { - components.Model.staticMethod = function() { - return 'Hello' - } - - components.Model.prototype.instanceProperty = ', world!' - - components.Model.prototype.instanceMethod = function() { - return `${this.$self().staticMethod()}${this.instanceProperty}` - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John' } - }) - - const user = store.getters['entities/users/find'](1) - - expect(user.id).toBe(1) - expect(user.instanceMethod()).toBe('Hello, world!') - }) - - it('add additional feature to the Query', async () => { - const plugin = { - install(components: any) { - components.Query.staticMethod = function() { - return 'Hello' - } - - components.Query.prototype.instanceMethod = function() { - return `${this.self().staticMethod()}, world!` - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John' } - }) - - const query = store.getters['entities/users/query']() - - expect(query.instanceMethod()).toBe('Hello, world!') - }) - - it('add additional feature to the rootGetters', () => { - const plugin = { - install(components: any) { - components.RootGetters.getName = (state: any) => { - return state.$name - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(store.getters['entities/getName']).toBe('entities') - }) - - it('add additional feature to the subGetters', () => { - const plugin = { - install(components: any) { - components.Getters.getName = (state: any) => { - return state.$name - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(store.getters['entities/users/getName']).toBe('users') - }) - - it('add additional feature to the rootActions', async () => { - const plugin = { - install(components: any) { - components.RootActions.getName = () => { - return 'John Doe' - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(await store.dispatch('entities/getName')).toBe('John Doe') - }) - - it('add additional feature to the subActions', async () => { - const plugin = { - install(components: any) { - components.Actions.getName = () => { - return 'John Doe' - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - expect(await store.dispatch('entities/users/getName')).toBe('John Doe') - }) - - it('add additional feature to the mutations', () => { - const plugin = { - install(components: any) { - components.RootMutations.setName = (state: any, name: any) => { - state.name = name - } - } - } - - VuexORM.use(plugin) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - store.commit('entities/setName', 'my_entities') - - expect(store.state.entities.name).toBe('my_entities') - }) - - it('can take extra options', () => { - const plugin = { - install(components: any, options: any) { - components.Query.version = () => options.version - } - } - - VuexORM.use(plugin, { version: '1.0.0' }) - - expect((Query as any).version()).toBe('1.0.0') - }) -}) diff --git a/test/feature/query/inserts_create.spec.ts b/test/feature/query/inserts_create.spec.ts deleted file mode 100644 index 15cbc475..00000000 --- a/test/feature/query/inserts_create.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { - createStore, - createState, - serializeCollection -} from 'test/support/Helpers' -import Uid from '@/support/Uid' -import { Model } from '@/index' - -describe('feature/query/inserts_create', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('Default Doe') - } - } - } - - beforeEach(() => { - Uid.reset() - }) - - it('can create a record', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ id: 1, name: 'John Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create multiple records', async () => { - const store = createStore([User]) - - await store.$repo(User).create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('replaces any existing records', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ id: 1, name: 'John Doe' }) - await store.$repo(User).create({ id: 2, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('cleans all existing records if the empty data is passed', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ id: 1, name: 'John Doe' }) - await store.$repo(User).create({}) - - const expected = createState({ - users: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('fills missing fields with the default value', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ id: 1 }) - - const expected = { - 1: { $id: '1', id: 1, name: 'Default Doe' } - } - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('can insert record with missing primary key value', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ name: 'John Doe' }) - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert record with primary key value of `null`', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ id: null, name: 'John Doe' }) - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns newly created data', async () => { - const store = createStore([User]) - - const result = await store.$repo(User).create([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(result.users.length).toBe(2) - expect(result.users[0]).toBeInstanceOf(User) - expect(result.users[1]).toBeInstanceOf(User) - expect(serializeCollection(result.users)).toEqual(expected) - }) - - // Test for deprecated payload API syntax. - it('accepts the "payload" object as an argument', async () => { - const store = createStore([User]) - - await store.$repo(User).create({ - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/query/inserts_insert.spec.ts b/test/feature/query/inserts_insert.spec.ts deleted file mode 100644 index afd6f247..00000000 --- a/test/feature/query/inserts_insert.spec.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { - createStore, - createState, - serializeCollection -} from 'test/support/Helpers' -import Uid from '@/support/Uid' -import { Model } from '@/index' - -describe('feature/query/inserts_insert', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('Default Doe') - } - } - } - - beforeEach(() => { - Uid.reset() - }) - - it('does nothing if the given data is empty', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({}) - - const expected = createState({ - users: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert a record', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({ id: 1, name: 'John Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert multiple records', async () => { - const store = createStore([User]) - - await store.$repo(User).insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('keeps existing records', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({ id: 1, name: 'John Doe' }) - await store.$repo(User).insert({ id: 2, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' }, - 2: { $id: '2', id: 2, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('fills missing fields with the default value', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({ id: 1 }) - - const expected = { - 1: { $id: '1', id: 1, name: 'Default Doe' } - } - - expect(store.state.entities.users.data).toEqual(expected) - }) - - it('can insert record with missing primary key value', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({ name: 'John Doe' }) - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can insert record with primary key value of `null`', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({ id: null, name: 'John Doe' }) - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns newly created data', async () => { - const store = createStore([User]) - - const result = await store.$repo(User).insert([ - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Jane Doe' } - ]) - - const expected = [ - { $id: '1', id: 1, name: 'John Doe' }, - { $id: '2', id: 2, name: 'Jane Doe' } - ] - - expect(result.users.length).toBe(2) - expect(result.users[0]).toBeInstanceOf(User) - expect(result.users[1]).toBeInstanceOf(User) - expect(serializeCollection(result.users)).toEqual(expected) - }) - - // Test for deprecated payload API syntax. - it('accepts the "payload" object as an argument', async () => { - const store = createStore([User]) - - await store.$repo(User).insert({ - data: { id: 1, name: 'John Doe' } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/query/inserts_new.spec.ts b/test/feature/query/inserts_new.spec.ts deleted file mode 100644 index 6b88da7d..00000000 --- a/test/feature/query/inserts_new.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Uid from '@/support/Uid' -import { Model } from '@/index' - -describe('feature/models/static_inserts_new', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.uid(), - name: this.attr('John Doe') - } - } - - id!: string - name!: string - } - - beforeEach(() => { - Uid.reset() - }) - - it('can create new data with all fields filled by default values', async () => { - const store = createStore([User]) - - await store.$repo(User).new() - await store.$repo(User).new() - - const expected = createState({ - users: { - $uid1: { $id: '$uid1', id: '$uid1', name: 'John Doe' }, - $uid2: { $id: '$uid2', id: '$uid2', name: 'John Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns a newly created model', async () => { - const store = createStore([User]) - - const user = await store.$repo(User).new() - - expect(user).toBeInstanceOf(User) - expect(user.$id).toBe('$uid1') - expect(user.id).toBe('$uid1') - expect(user.name).toBe('John Doe') - }) -}) diff --git a/test/feature/relations/BelongsTo.spec.ts b/test/feature/relations/BelongsTo.spec.ts deleted file mode 100644 index 5b7583a9..00000000 --- a/test/feature/relations/BelongsTo.spec.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Belongs To', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id') - } - } - } - - it('can create data containing the belongs to relation', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user_id: 1, - user: { id: 1 } - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1 } - }, - posts: { - 1: { $id: '1', id: 1, user_id: 1, user: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data when the belongs to relation is `null`', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user_id: 1, - user: null - } - }) - - const expected = createState({ - users: {}, - posts: { - 1: { $id: '1', id: 1, user_id: 1, user: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data when the belongs to relation is set to related model key', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user: 1 - } - }) - - const expected = createState({ - users: {}, - posts: { - 1: { $id: '1', id: 1, user_id: 1, user: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can generate relation field', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user: { id: 1 } - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1 } - }, - posts: { - 1: { $id: '1', id: 1, user_id: 1, user: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('returns created record from `create` method', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - const result = await store.dispatch('entities/posts/create', { - data: { - id: 1, - user_id: 1, - user: { id: 1 } - } - }) - - const expected = { - users: [{ $id: '1', id: 1 }], - posts: [{ $id: '1', id: 1, user_id: 1, user: null }] - } - - expect(result).toEqual(expected) - }) - - it('can resolve the belongs to relation', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user: { id: 1 } - } - }) - - const expected = { - $id: '1', - id: 1, - user_id: 1, - user: { - $id: '1', - id: 1 - } - } - - const post = store.getters['entities/posts/query']() - .with('user') - .find(1) - - expect(post).toEqual(expected) - }) - - it('resolves to null if there is no belongs to relation', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user_id: 1 - } - }) - - const expected = { - $id: '1', - id: 1, - user_id: 1, - user: null - } - - const post = store.getters['entities/posts/query']() - .with('user') - .find(1) - - expect(post).toEqual(expected) - }) - - it('can resolve belongs to relation which its id is 0', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user: { id: 0 } - } - }) - - const expected = { - $id: '1', - id: 1, - user_id: 0, - user: { - $id: '0', - id: 0 - } - } - - const post = store.getters['entities/posts/query']() - .with('user') - .first() - - expect(post).toEqual(expected) - }) - - it('can resolve belongs to relation with custom foreign key', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id', 'post_id') - } - } - } - - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/posts/create', { - data: { - id: 1, - user_id: 2, - user: { id: 1, post_id: 2 } - } - }) - - const expected = { - $id: '1', - id: 1, - user_id: 2, - user: { - $id: '1', - id: 1, - post_id: 2 - } - } - - const post = store.getters['entities/posts/query']() - .with('user') - .find(1) - - expect(post).toEqual(expected) - }) -}) diff --git a/test/feature/relations/BelongsToMany_Persist.spec.ts b/test/feature/relations/BelongsToMany_Persist.spec.ts deleted file mode 100644 index 854aec49..00000000 --- a/test/feature/relations/BelongsToMany_Persist.spec.ts +++ /dev/null @@ -1,564 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Relations – Belongs To Many – Persist', () => { - it('can create a data with belongs to many relation', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - permissions: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null), - level: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/create', { - data: { - id: 1, - permissions: [{ id: 1, pivot: { level: 1 } }, { id: 2 }] - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, permissions: [] } - }, - roles: { - 1: { $id: '1', id: 1 }, - 2: { $id: '2', id: 2 } - }, - roleUser: { - '[1,1]': { $id: '[1,1]', role_id: 1, user_id: 1, level: 1 }, - '[2,1]': { $id: '[2,1]', role_id: 2, user_id: 1, level: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a data with belongs to many relation where pivot composite key reversed', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - permissions: this.belongsToMany(Role, UserRole, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class UserRole extends Model { - static entity = 'userRole' - - static primaryKey = ['user_id', 'role_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: UserRole } - ]) - - await store.dispatch('entities/users/create', { - data: { - id: 1, - permissions: [{ id: 1 }, { id: 2 }] - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, permissions: [] } - }, - roles: { - 1: { $id: '1', id: 1 }, - 2: { $id: '2', id: 2 } - }, - userRole: { - '[1,1]': { $id: '[1,1]', role_id: 1, user_id: 1 }, - '[1,2]': { $id: '[1,2]', role_id: 2, user_id: 1 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a data without relation', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/create', { data: { id: 1 } }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, roles: [] } - }, - roles: {}, - roleUser: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a belongs to many relation data from nested data', async () => { - class Team extends Model { - static entity = 'teams' - - static fields() { - return { - id: this.attr(null), - users: this.hasMany(User, 'team_id') - } - } - } - - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - team_id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null), - users: this.belongsToMany(User, RoleUser, 'role_id', 'user_id') - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null), - level: this.attr(null) - } - } - } - - const store = createStore([ - { model: Team }, - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await Team.create({ - data: { - id: 1, - users: [ - { - team_id: 1, - id: 1, - roles: [{ id: 2, pivot: { level: 1 } }, { id: 3 }] - } - ] - } - }) - - expect(store.state.entities.teams.data['1'].id).toBe(1) - expect(store.state.entities.users.data['1'].id).toBe(1) - expect(store.state.entities.roles.data['2'].id).toBe(2) - expect(store.state.entities.roles.data['3'].id).toBe(3) - expect(store.state.entities.roleUser.data['[2,1]'].$id).toStrictEqual( - '[2,1]' - ) - expect(store.state.entities.roleUser.data['[2,1]'].level).toBe(1) - expect(store.state.entities.roleUser.data['[3,1]'].$id).toStrictEqual( - '[3,1]' - ) - expect(store.state.entities.roleUser.data['[3,1]'].level).toBe(null) - }) - - it('can retrieve data by filtering with `whereHas`', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - await store.dispatch('entities/roles/create', { - data: [{ id: 1 }, { id: 2 }] - }) - - await store.dispatch('entities/roleUser/create', { - data: [ - { role_id: 1, user_id: 1 }, - { role_id: 2, user_id: 2 } - ] - }) - - const users = store.getters['entities/users/query']() - .whereHas('roles', (query: Query) => { - query.where('id', 2) - }) - .get() - - expect(users.length).toBe(1) - expect(users[0].id).toBe(2) - }) - - it('can create a data to pivot table with field', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - roleUsers: this.hasMany(RoleUser, 'user_id'), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'roleUsers' - - static primaryKey = 'id' - - static fields() { - return { - id: this.attr(null), - role_id: this.attr(null), - user_id: this.attr(null), - type: this.attr('') - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/insert', { - data: { - id: 1, - name: 'Jane Doe', - roleUsers: [ - { id: 1, role_id: 1, user_id: 1, type: 'administrator' }, - { id: 2, role_id: 1, user_id: 1, type: 'general' }, - { id: 3, role_id: 2, user_id: 1, type: 'general' } - ], - roles: [{ id: 1 }, { id: 2 }] - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe', roleUsers: [], roles: [] } - }, - roles: { - 1: { $id: '1', id: 1 }, - 2: { $id: '2', id: 2 } - }, - roleUsers: { - 1: { $id: '1', id: 1, user_id: 1, role_id: 1, type: 'administrator' }, - 2: { $id: '2', id: 2, user_id: 1, role_id: 1, type: 'general' }, - 3: { $id: '3', id: 3, user_id: 1, role_id: 2, type: 'general' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a data to pivot table with field using composite key', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - roleUsers: this.hasMany(RoleUser, 'user_id'), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'roleUsers' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null), - type: this.attr('') - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/insert', { - data: { - id: 1, - name: 'Jane Doe', - roleUsers: [ - { role_id: 1, user_id: 1, type: 'administrator' }, - { role_id: 2, user_id: 1, type: 'general' } - ], - roles: [{ id: 1 }, { id: 2 }] - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe', roleUsers: [], roles: [] } - }, - roles: { - 1: { $id: '1', id: 1 }, - 2: { $id: '2', id: 2 } - }, - roleUsers: { - '[1,1]': { - $id: '[1,1]', - user_id: 1, - role_id: 1, - type: 'administrator' - }, - '[2,1]': { $id: '[2,1]', user_id: 1, role_id: 2, type: 'general' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a data with belongs to many relation with pivot data having custom key', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - permissions: this.belongsToMany( - Role, - RoleUser, - 'user_id', - 'role_id' - ).as('role_user') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null), - level: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/insert', { - data: { - id: 1, - permissions: [{ id: 1, role_user: { level: 1 } }, { id: 2 }] - } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, permissions: [] } - }, - roles: { - 1: { $id: '1', id: 1 }, - 2: { $id: '2', id: 2 } - }, - roleUser: { - '[1,1]': { $id: '[1,1]', role_id: 1, user_id: 1, level: 1 }, - '[2,1]': { $id: '[2,1]', role_id: 2, user_id: 1, level: null } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/relations/BelongsToMany_Retrieve.spec.ts b/test/feature/relations/BelongsToMany_Retrieve.spec.ts deleted file mode 100644 index a2e7a44f..00000000 --- a/test/feature/relations/BelongsToMany_Retrieve.spec.ts +++ /dev/null @@ -1,453 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Belongs To Many – Retrieve', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @BelongsToMany(Role, RoleUser, 'user_id', 'role_id') - roles!: Role[] - - static fields() { - return { - id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - // @Attribute - id!: number - - // @BelongsToMany(User, RoleUser, 'role_id', 'user_id') - users!: User[] - - // @Intermediate - pivot!: RoleUser - - static fields() { - return { - id: this.attr(null), - users: this.belongsToMany(User, RoleUser, 'role_id', 'user_id') - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - // @Attribute - role_id!: number - - // @Attribute - user_id!: number - - // @Attribute - level!: number - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null), - level: this.attr(null) - } - } - } - - it('can resolve belongs to many relation', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create([{ id: 1 }, { id: 2 }]) - - await Role.create([{ id: 2 }, { id: 3 }]) - - await RoleUser.create([ - { user_id: 1, role_id: 2 }, - { user_id: 1, role_id: 3 } - ]) - - const user = User.query() - .with('roles') - .find(1) as User - - expect(user.roles.length).toBe(2) - expect(user.roles[0].id).toBe(2) - expect(user.roles[1].id).toBe(3) - - const userWithoutRoles = User.query() - .with('roles') - .find(2) as User - expect(userWithoutRoles.roles.length).toBe(0) - }) - - it('can resolve belongs to many relation with pivot data', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create([ - { - id: 1, - roles: [ - { id: 2, pivot: { level: 1 } }, - { id: 3, pivot: { level: 2 } } - ] - }, - { id: 2 } - ]) - - const user = User.query() - .with('roles') - .find(1) as User - - expect(user.roles.length).toBe(2) - expect(user.roles[0].id).toBe(2) - expect(user.roles[0].pivot.level).toBe(1) - expect(user.roles[1].id).toBe(3) - expect(user.roles[1].pivot.level).toBe(2) - - const userWithoutRoles = User.query() - .with('roles') - .find(2) as User - expect(userWithoutRoles.roles.length).toBe(0) - }) - - it('can resolve inverse belongs to many relation', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create({ id: 1 }) - - await Role.create([{ id: 2 }, { id: 3 }]) - - await RoleUser.create([ - { user_id: 1, role_id: 2 }, - { user_id: 1, role_id: 3 } - ]) - - const role = Role.query() - .with('users') - .find(2) as Role - - expect(role.users.length).toBe(1) - expect(role.users[0].id).toBe(1) - }) - - it('returns empty collection if the relation is not found', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create({ id: 1 }) - - const user = User.query() - .with('roles') - .first() as User - - expect(user.roles).toEqual([]) - }) - - it('returns empty collection if the Role relation from RoleUser is not found', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create({ id: 1 }) - - await RoleUser.create([{ user_id: 1 }]) - - const user = User.query() - .with('roles') - .first() as User - - expect(user.roles).toEqual([]) - }) - - it('can resolve belongs to many relation with has constraint', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create({ id: 1 }) - - await Role.create([{ id: 2 }, { id: 3 }]) - - await RoleUser.create([ - { user_id: 1, role_id: 2 }, - { user_id: 1, role_id: 3 }, - { user_id: 2, role_id: 4 } - ]) - - const roles = Role.query() - .has('users') - .get() - - expect(roles.length).toBe(2) - }) - - it('can resolve nested belongs to relation', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @BelongsToMany(Role, RoleUser, 'user_id', 'role_id') - roles!: Role[] - - // @Intermediate - pivot!: RoleUser - - static fields() { - return { - id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - // @Attribute - id!: number - - // @BelongsToMany(Permission, RolePermission, 'role_id', 'permission_id') - permissions!: Permission[] - - // @Intermediate - pivot!: RoleUser - - static fields() { - return { - id: this.attr(null), - permissions: this.belongsToMany( - Permission, - RolePermission, - 'role_id', - 'permission_id' - ) - } - } - } - - class Permission extends Model { - static entity = 'permissions' - - // @Attribute - id!: number - - // @Intermediate - pivot!: RolePermission - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'role_users' - - static primaryKey = ['role_id', 'user_id'] - - // @Attribute - role_id!: number - - // @Attribute - user_id!: number - - // @Attribute - level!: number - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null), - level: this.attr(null) - } - } - } - - class RolePermission extends Model { - static entity = 'role_permissions' - - static primaryKey = ['role_id', 'permission_id'] - - // @Attribute - role_id!: number - - // @Attribute - permission_id!: number - - // @Attribute - type!: string - - static fields() { - return { - role_id: this.attr(null), - permission_id: this.attr(null), - type: this.attr(null) - } - } - } - - createStore([ - { model: User }, - { model: Role }, - { model: Permission }, - { model: RoleUser }, - { model: RolePermission } - ]) - - const data = [ - { - id: 1, - roles: [ - { - id: 1, - permissions: [ - { id: 1, pivot: { type: 'can' } }, - { id: 2, pivot: { type: 'cant' } }, - { id: 3 }, - { id: 4 } - ], - pivot: { level: 1 } - } - ] - }, - { - id: 2, - roles: [ - { - id: 2, - permissions: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }] - } - ] - } - ] - - await User.create(data) - - const users = User.query() - .with('roles.permissions') - .get() as User[] - - expect(users[0].id).toBe(1) - expect(users[0].roles.length).toBe(1) - expect(users[0].roles[0].id).toBe(1) - expect(users[0].roles[0].pivot.level).toBe(1) - expect(users[0].roles[0].permissions.length).toBe(4) - expect(users[0].roles[0].permissions[0].id).toBe(1) - expect(users[0].roles[0].permissions[0].pivot.type).toBe('can') - expect(users[0].roles[0].permissions[1].id).toBe(2) - expect(users[0].roles[0].permissions[1].pivot.type).toBe('cant') - expect(users[0].roles[0].permissions[2].pivot.type).toBe(null) - - expect(users[1].id).toBe(2) - expect(users[1].roles.length).toBe(1) - expect(users[1].roles[0].id).toBe(2) - expect(users[1].roles[0].permissions.length).toBe(4) - expect(users[1].roles[0].permissions[0].id).toBe(1) - expect(users[1].roles[0].permissions[1].id).toBe(2) - }) - - it('can resolve belongs to relation with primary key set to id', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @BelongsToMany(Role, RoleUser, 'user_id', 'role_id') - roles!: Role[] - - static fields() { - return { - id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class RoleUser extends Model { - static entity = 'role_users' - - // @Attribute - id!: number - - // @Attribute - role_id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - role_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create([{ id: 1 }]) - - await Role.create({ id: 1 }) - - await RoleUser.create({ id: 1, user_id: 1, role_id: 1 }) - - const users = User.query() - .with('roles') - .get() - - expect(users[0].id).toBe(1) - expect(users[0].roles.length).toBe(1) - expect(users[0].roles[0].id).toBe(1) - }) - - it('can apply `orderBy` constraint on nested relations', async () => { - createStore([{ model: User }, { model: Role }, { model: RoleUser }]) - - await User.create({ - id: 1, - roles: [{ id: 1 }, { id: 3 }, { id: 2 }] - }) - - const ascending = User.query() - .with('roles', (query) => { - query.orderBy('id', 'asc') - }) - .find(1) as User - - expect(ascending.roles[0].id).toBe(1) - expect(ascending.roles[1].id).toBe(2) - expect(ascending.roles[2].id).toBe(3) - - const descending = User.query() - .with('roles', (query) => { - query.orderBy('id', 'desc') - }) - .find(1) as User - - expect(descending.roles[0].id).toBe(3) - expect(descending.roles[1].id).toBe(2) - expect(descending.roles[2].id).toBe(1) - }) -}) diff --git a/test/feature/relations/HasMany.spec.ts b/test/feature/relations/HasMany.spec.ts deleted file mode 100644 index ec8fa695..00000000 --- a/test/feature/relations/HasMany.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Has Many', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - it('can create data containing the has many relation', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create({ - id: 1, - posts: [{ id: 1 }, { id: 2 }] - }) - - const expectedUsers = [{ $id: '1', id: 1, posts: [] }] - - expect(User.all()).toEqual(expectedUsers) - - const expectedPosts = [ - { $id: '1', id: 1, user_id: 1 }, - { $id: '2', id: 2, user_id: 1 } - ] - - expect(Post.all()).toEqual(expectedPosts) - }) - - it('can update data and insert has many relation', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await User.update( - { - id: 1, - posts: [{ id: 1 }, { id: 2 }] - }, - { - insert: ['posts'] - } - ) - - const expectedUsers = [{ $id: '1', id: 1, posts: [] }] - - expect(User.all()).toEqual(expectedUsers) - - const expectedPosts = [ - { $id: '1', id: 1, user_id: 1 }, - { $id: '2', id: 2, user_id: 1 } - ] - - expect(Post.all()).toEqual(expectedPosts) - }) - - it('can resolve the has many relation', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([ - { - id: 1, - posts: [{ id: 1 }, { id: 2 }] - }, - { - id: 2, - posts: null - } - ]) - - const expected = [ - { - $id: '1', - id: 1, - posts: [ - { $id: '1', id: 1, user_id: 1 }, - { $id: '2', id: 2, user_id: 1 } - ] - }, - { - $id: '2', - id: 2, - posts: [] - } - ] - - const user = User.query() - .with('posts') - .all() - - expect(user).toEqual(expected) - }) - - it('can resolve the has many relation with custom primary key', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = 'user_id' - - static fields() { - return { - user_id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.create({ - user_id: 1, - posts: [{ id: 1 }, { id: 2 }] - }) - - const expected = { - $id: '1', - user_id: 1, - posts: [ - { $id: '1', id: 1, user_id: 1 }, - { $id: '2', id: 2, user_id: 1 } - ] - } - - const user = User.query() - .with('posts') - .find(1) - - expect(user).toEqual(expected) - }) - - it('can resolve the has many using the local key', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - local_key: this.attr(null), - posts: this.hasMany(Post, 'user_id', 'local_key') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.create({ - id: 1, - local_key: 'local_key', - posts: [{ id: 1 }] - }) - - const expectedUsers = [ - { $id: '1', id: 1, local_key: 'local_key', posts: [] } - ] - - expect(User.all()).toEqual(expectedUsers) - - const expectedPosts = [{ $id: '1', id: 1, user_id: 'local_key' }] - - expect(Post.all()).toEqual(expectedPosts) - }) -}) diff --git a/test/feature/relations/HasManyBy.spec.ts b/test/feature/relations/HasManyBy.spec.ts deleted file mode 100644 index 80ec2524..00000000 --- a/test/feature/relations/HasManyBy.spec.ts +++ /dev/null @@ -1,284 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Has Many By', () => { - it('can create data containing the has many by relation', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - post_ids: this.attr([]), - posts: this.hasManyBy(Post, 'post_ids') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([{ model: User }, { model: Post }]) - - await User.insert({ - id: 1, - posts: [{ id: 1 }, { id: 2 }] - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, post_ids: [1, 2], posts: [] } - }, - posts: { - 1: { $id: '1', id: 1 }, - 2: { $id: '2', id: 2 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data containing empty has many by relation', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - post_ids: this.attr([]), - posts: this.hasManyBy(Post, 'posts') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([{ model: User }, { model: Post }]) - - await User.insert({ id: 1, posts: [] }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, post_ids: [], posts: [] } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data containing has many by key with `null`', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - post_ids: this.attr([]), - posts: this.hasManyBy(Post, 'posts') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([{ model: User }, { model: Post }]) - - await User.insert({ id: 1, posts: null }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, post_ids: [], posts: [] } - }, - posts: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can resolve the has many by relation', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - post_ids: this.attr([]), - posts: this.hasManyBy(Post, 'post_ids') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.insert({ - id: 1, - posts: [{ id: 1 }, { id: 2 }] - }) - - const user = User.query() - .with('posts') - .find(1) - - const expected = { - $id: '1', - id: 1, - post_ids: [1, 2], - posts: [ - { $id: '1', id: 1 }, - { $id: '2', id: 2 } - ] - } - - expect(user).toEqual(expected) - }) - - it('can resolve the has many by relation with mixed existence', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - post_ids: this.attr([]), - posts: this.hasManyBy(Post, 'post_ids') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.insert([ - { id: 1, posts: [{ id: 1 }, { id: 2 }] }, - { id: 2, post_ids: [], posts: [] } - ]) - - const users = User.query() - .with('posts') - .get() - - const expected = [ - { - $id: '1', - id: 1, - post_ids: [1, 2], - posts: [ - { $id: '1', id: 1 }, - { $id: '2', id: 2 } - ] - }, - { - $id: '2', - id: 2, - post_ids: [], - posts: [] - } - ] - - expect(users).toEqual(expected) - }) - - it('can apply `orderBy` constraint on nested relations', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute([]) - post_ids!: number[] - - // @HasManyBy(Post, 'post_ids') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - post_ids: this.attr([]), - posts: this.hasManyBy(Post, 'post_ids') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.insert({ id: 1, posts: [{ id: 1 }, { id: 3 }, { id: 2 }] }) - - const ascending = User.query() - .with('posts', (query) => { - query.orderBy('id', 'asc') - }) - .find(1) as User - - expect(ascending.posts[0].id).toBe(1) - expect(ascending.posts[1].id).toBe(2) - expect(ascending.posts[2].id).toBe(3) - - const descending = User.query() - .with('posts', (query) => { - query.orderBy('id', 'desc') - }) - .find(1) as User - - expect(descending.posts[0].id).toBe(3) - expect(descending.posts[1].id).toBe(2) - expect(descending.posts[2].id).toBe(1) - }) -}) diff --git a/test/feature/relations/HasManyThrough.spec.ts b/test/feature/relations/HasManyThrough.spec.ts deleted file mode 100644 index 98507929..00000000 --- a/test/feature/relations/HasManyThrough.spec.ts +++ /dev/null @@ -1,572 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Has Many Through', () => { - it('can create data contains has many through relationship', async () => { - class Country extends Model { - static entity = 'countries' - - // @Attribute - id!: number - - // @HasManyThrough(Post, User, 'country_id', 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute - country_id!: number - - static fields() { - return { - id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const store = createStore([ - { model: Country }, - { model: User }, - { model: Post } - ]) - - await Country.insert({ - id: 1, - posts: [{ id: 1 }, { id: 2 }] - }) - - const expected = createState({ - countries: { - 1: { $id: '1', id: 1, posts: [] } - }, - posts: { - 1: { $id: '1', id: 1, user_id: null }, - 2: { $id: '2', id: 2, user_id: null } - }, - users: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can resolve has many through relationship', async () => { - class Country extends Model { - static entity = 'countries' - - // @Attribute - id!: number - - // @HasManyThrough(Post, User, 'country_id', 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute - country_id!: number - - static fields() { - return { - id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: Country }, { model: User }, { model: Post }]) - - await User.insert([ - { id: 1, country_id: 1 }, - { id: 2, country_id: 1 }, - { id: 3, country_id: 2 } - ]) - - await Post.insert([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 3 }, - { id: 4, user_id: 1 } - ]) - - await Country.insert([{ id: 1 }, { id: 2 }]) - - const country = Country.query() - .with('posts') - .find(1) as Country - - expect(country).toBeInstanceOf(Country) - expect(country.posts.length).toBe(3) - expect(country.posts[0]).toBeInstanceOf(Post) - - // user_id: 1 - expect(country.posts[0].user_id).toBe(1) - expect(country.posts[1].user_id).toBe(1) - expect(country.posts[0].id).toBe(1) - expect(country.posts[1].id).toBe(4) - - // user_id: 2 - expect(country.posts[2].user_id).toBe(2) - expect(country.posts[2].id).toBe(2) - }) - - it('can resolve has many through relationship with string value id', async () => { - class Country extends Model { - static entity = 'countries' - - // @Str('') - id!: string - - // @HasManyThrough(Post, User, 'country_id', 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.string(''), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - // @Str('') - id!: string - - // @Str('') - country_id!: string - - static fields() { - return { - id: this.string(''), - country_id: this.string('') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Str('') - id!: string - - // @Str('') - user_id!: string - - static fields() { - return { - id: this.string(''), - user_id: this.string('') - } - } - } - - createStore([{ model: Country }, { model: User }, { model: Post }]) - - await User.insert([ - { id: 'string-id-1', country_id: 'string-id-1' }, - { id: 'string-id-2', country_id: 'string-id-1' }, - { id: 'string-id-3', country_id: 'string-id-2' } - ]) - - await Post.insert([ - { id: 'string-id-1', user_id: 'string-id-1' }, - { id: 'string-id-2', user_id: 'string-id-2' }, - { id: 'string-id-3', user_id: 'string-id-3' }, - { id: 'string-id-4', user_id: 'string-id-1' } - ]) - - await Country.insert([{ id: 'string-id-1' }, { id: 'string-id-2' }]) - - const country = Country.query() - .with('posts') - .find('string-id-1') as Country - - expect(country).toBeInstanceOf(Country) - expect(country.posts.length).toBe(3) - expect(country.posts[0]).toBeInstanceOf(Post) - - expect(country.posts[0].user_id).toBe('string-id-1') - expect(country.posts[1].user_id).toBe('string-id-1') - expect(country.posts[2].user_id).toBe('string-id-2') - expect(country.posts[0].id).toBe('string-id-1') - expect(country.posts[1].id).toBe('string-id-4') - expect(country.posts[2].id).toBe('string-id-2') - }) - - it('can resolve the relationship when the intermediate entity is empty', async () => { - class Country extends Model { - static entity = 'countries' - - // @Attribute - id!: number - - // @HasManyThrough(Post, User, 'country_id', 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute - country_id!: number - - static fields() { - return { - id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: Country }, { model: User }, { model: Post }]) - - await Country.insert([{ id: 1 }]) - - const country = Country.query() - .with('posts') - .find(1) as Country - - expect(country.posts.length).toBe(0) - }) - - it('can resolve the relationship when the target entity is empty', async () => { - class Country extends Model { - static entity = 'countries' - - // @Attribute - id!: number - - // @HasManyThrough(Post, User, 'country_id', 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute - country_id!: number - - static fields() { - return { - id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: Country }, { model: User }, { model: Post }]) - - await User.insert({ - id: 1, - country_id: 1 - }) - - await Country.insert([{ id: 1 }]) - - const country = Country.query() - .with('posts') - .find(1) as Country - - expect(country.posts.length).toBe(0) - }) - - it('can resolve has many through relationship with custom primary keys', async () => { - class Country extends Model { - static entity = 'countries' - - static primaryKey = 'c_id' - - // @Attribute - c_id!: number - - // @HasManyThrough(Post, User, 'country_id', 'user_id') - posts!: Post[] - - static fields() { - return { - c_id: this.attr(null), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - static primaryKey = 'u_id' - - // @Attribute - u_id!: number - - // @Attribute - country_id!: number - - static fields() { - return { - u_id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - static primaryKey = 'p_id' - - // @Attribute - p_id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - p_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: Country }, { model: User }, { model: Post }]) - - await User.insert([ - { u_id: 1, country_id: 1 }, - { u_id: 2, country_id: 1 }, - { u_id: 3, country_id: 2 } - ]) - - await Post.insert([ - { p_id: 1, user_id: 1 }, - { p_id: 2, user_id: 2 }, - { p_id: 3, user_id: 3 } - ]) - - await Country.insert([{ c_id: 1 }, { c_id: 2 }]) - - const country = Country.query() - .with('posts') - .find(1) as Country - - expect(country).toBeInstanceOf(Country) - expect(country.posts.length).toBe(2) - expect(country.posts[0]).toBeInstanceOf(Post) - expect(country.posts[0].p_id).toBe(1) - expect(country.posts[1].p_id).toBe(2) - }) - - it('can resolve has many through relationship with custom local keys', async () => { - class Country extends Model { - static entity = 'countries' - - // @Attribute - id!: number - - // @Attribute - c_id!: number - - // @HasManyThrough(Post, User, 'country_id', 'user_id', 'c_id', 'u_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - c_id: this.attr(null), - posts: this.hasManyThrough( - Post, - User, - 'country_id', - 'user_id', - 'c_id', - 'u_id' - ) - } - } - } - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute - u_id!: number - - // @Attribute - country_id!: number - - static fields() { - return { - id: this.attr(null), - u_id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - p_id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - p_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: Country }, { model: User }, { model: Post }]) - - await User.insert([ - { id: 11, u_id: 1, country_id: 1 }, - { id: 12, u_id: 2, country_id: 1 }, - { id: 13, u_id: 3, country_id: 2 } - ]) - - await Post.insert([ - { id: 11, p_id: 1, user_id: 1 }, - { id: 12, p_id: 2, user_id: 2 }, - { id: 13, p_id: 3, user_id: 3 } - ]) - - await Country.insert([ - { id: 11, c_id: 1 }, - { id: 12, c_id: 2 } - ]) - - const country = Country.query() - .with('posts') - .find(11) as Country - - expect(country).toBeInstanceOf(Country) - expect(country.posts.length).toBe(2) - expect(country.posts[0]).toBeInstanceOf(Post) - expect(country.posts[0].id).toBe(11) - expect(country.posts[1].id).toBe(12) - }) -}) diff --git a/test/feature/relations/HasOne.spec.ts b/test/feature/relations/HasOne.spec.ts deleted file mode 100644 index 542035b4..00000000 --- a/test/feature/relations/HasOne.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model, Fields } from '@/index' - -describe('Features – Relations – Has One', () => { - class User extends Model { - static entity = 'users' - - static fields(): Fields { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - static fields(): Fields { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - it('can create data containing the has one relation', async () => { - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert({ - id: 1, - phone: { id: 1, user_id: 1 } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, phone: null } - }, - phones: { - 1: { $id: '1', id: 1, user_id: 1 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data when the has one relation is empty', async () => { - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert({ $id: '1', id: 1 }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, phone: null } - }, - phones: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data when the has one relation is `null`', async () => { - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert({ - id: 1, - phone: null - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, phone: null } - }, - phones: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create data when the has one relation is set to primitive value', async () => { - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert({ id: 1, phone: 1 }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, phone: null } - }, - phones: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('it can create data containing mixed fields value', async () => { - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert([ - { - id: 1, - phone: { id: 1, user_id: 1 } - }, - { - id: 2, - phone: null - } - ]) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, phone: null }, - 2: { $id: '2', id: 2, phone: null } - }, - phones: { - 1: { $id: '1', id: 1, user_id: 1 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('assigns local key if it is not present when creating the record', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - local_key: this.attr(null), - phone: this.hasOne(Phone, 'user_id', 'local_key') - } - } - } - - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert({ - id: 1, - phone: { id: 1 } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, local_key: '1', phone: null } - }, - phones: { - 1: { $id: '1', id: 1, user_id: '1' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can resolve the has one relation', async () => { - createStore([{ model: User }, { model: Phone }]) - - await User.insert({ - id: 1, - phone: { id: 1 } - }) - - const expected = { - $id: '1', - id: 1, - phone: { - $id: '1', - id: 1, - user_id: 1 - } - } - - const user = User.query() - .with('phone') - .find(1) - - expect(user).toEqual(expected) - expect(user).toBeInstanceOf(User) - }) - - it('can resolve the empty has one relation', async () => { - const store = createStore([{ model: User }, { model: Phone }]) - - await User.insert({ id: 1 }) - - const expected = { $id: '1', id: 1, phone: null } - - const user = store.getters['entities/users/query']() - .with('phone') - .find(1) - - expect(user).toEqual(expected) - }) -}) diff --git a/test/feature/relations/HasOneThrough.spec.ts b/test/feature/relations/HasOneThrough.spec.ts deleted file mode 100644 index 89831179..00000000 --- a/test/feature/relations/HasOneThrough.spec.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Has One Through', () => { - it('can create entities containing the relation', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute() - id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') - image!: Image - - static fields() { - return { - id: this.attr(null), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Attribute() - id!: number - - // @Attribute() - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - // @Attribute() - id!: number - - // @Attribute() - profile_id!: number - - static fields() { - return { - id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Profile }, - { model: Image } - ]) - - await User.insert({ - id: 1, - image: { id: 1 } - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, image: null } - }, - images: { - 1: { $id: '1', id: 1, profile_id: null } - }, - profiles: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can resolve the relation', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute() - id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') - image!: Image - - static fields() { - return { - id: this.attr(null), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Attribute() - id!: number - - // @Attribute() - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - // @Attribute() - id!: number - - // @Attribute() - profile_id!: number - - static fields() { - return { - id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Profile }, { model: Image }]) - - await User.insert([{ id: 1 }, { id: 2 }]) - - await Profile.insert([ - { id: 3, user_id: 1 }, - { id: 4, user_id: 2 } - ]) - - await Image.insert([ - { id: 5, profile_id: 3 }, - { id: 6, profile_id: 4 } - ]) - - const user = User.query() - .with('image') - .find(1) as User - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe(1) - expect(user.image).toBeInstanceOf(Image) - expect(user.image.profile_id).toBe(3) - expect(user.image.id).toBe(5) - }) - - it('can resolve the relation with string value id', async () => { - class User extends Model { - static entity = 'users' - - // @Str('') - id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') - image!: Image - - static fields() { - return { - id: this.string(''), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Str('') - id!: number - - // @Str('') - user_id!: number - - static fields() { - return { - id: this.string(''), - user_id: this.string('') - } - } - } - - class Image extends Model { - static entity = 'images' - - // @Str('') - id!: number - - // @Str('') - profile_id!: number - - static fields() { - return { - id: this.string(''), - profile_id: this.string('') - } - } - } - - createStore([{ model: User }, { model: Profile }, { model: Image }]) - - await User.insert([{ id: 'string-id-1' }, { id: 'string-id-2' }]) - - await Profile.insert([ - { id: 'string-id-3', user_id: 'string-id-1' }, - { id: 'string-id-4', user_id: 'string-id-2' } - ]) - - await Image.insert([ - { id: 'string-id-5', profile_id: 'string-id-3' }, - { id: 'string-id-6', profile_id: 'string-id-4' } - ]) - - const user = User.query() - .with('image') - .find('string-id-1') as User - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe('string-id-1') - expect(user.image).toBeInstanceOf(Image) - expect(user.image.profile_id).toBe('string-id-3') - expect(user.image.id).toBe('string-id-5') - }) - - it('can resolve the relation when the intermediate entity is empty', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute() - id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') - image!: Image - - static fields() { - return { - id: this.attr(null), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Attribute() - id!: number - - // @Attribute() - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - // @Attribute() - id!: number - - // @Attribute() - profile_id!: number - - static fields() { - return { - id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Profile }, { model: Image }]) - - await User.insert([{ id: 1 }]) - - const user = User.query() - .with('image') - .find(1) as User - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe(1) - expect(user.image).toBeNull() - }) - - it('can resolve the relation when the target entity is empty', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute() - id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') - image!: Image - - static fields() { - return { - id: this.attr(null), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Attribute() - id!: number - - // @Attribute() - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - // @Attribute() - id!: number - - // @Attribute() - profile_id!: number - - static fields() { - return { - id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Profile }, { model: Image }]) - - await User.insert({ id: 1 }) - - await Profile.insert({ - id: 1, - user_id: 1 - }) - - const user = User.query() - .with('image') - .find(1) as User - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe(1) - expect(user.image).toBeNull() - }) - - it('can resolve the relation with custom primary keys', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = 'u_id' - - // @Attribute() - u_id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id') - image!: Image - - static fields() { - return { - u_id: this.attr(null), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - static primaryKey = 'p_id' - - // @Attribute() - p_id!: number - - // @Attribute() - user_id!: number - - static fields() { - return { - p_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - static primaryKey = 'i_id' - - // @Attribute() - i_id!: number - - // @Attribute() - profile_id!: number - - static fields() { - return { - i_id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Profile }, { model: Image }]) - - await User.insert([{ u_id: 1 }, { u_id: 2 }]) - - await Profile.insert([ - { p_id: 3, user_id: 1 }, - { p_id: 4, user_id: 2 } - ]) - - await Image.insert([ - { i_id: 5, profile_id: 3 }, - { i_id: 6, profile_id: 4 } - ]) - - const user = User.query() - .with('image') - .find(1) as User - - expect(user).toBeInstanceOf(User) - expect(user.u_id).toBe(1) - expect(user.image).toBeInstanceOf(Image) - expect(user.image.profile_id).toBe(3) - expect(user.image.i_id).toBe(5) - }) - - it('can resolve the relation with custom local keys', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute() - id!: number - - // @Attribute() - u_id!: number - - // @HasOneThrough(Image, Profile, 'user_id', 'profile_id', 'u_id', 'p_id') - image!: Image - - static fields() { - return { - id: this.attr(null), - u_id: this.attr(null), - image: this.hasOneThrough( - Image, - Profile, - 'user_id', - 'profile_id', - 'u_id', - 'p_id' - ) - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Attribute() - id!: number - - // @Attribute() - p_id!: number - - // @Attribute() - user_id!: number - - static fields() { - return { - id: this.attr(null), - p_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - // @Attribute() - id!: number - - // @Attribute() - i_id!: number - - // @Attribute() - profile_id!: number - - static fields() { - return { - id: this.attr(null), - i_id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Profile }, { model: Image }]) - - await User.insert([ - { id: 11, u_id: 1 }, - { id: 12, u_id: 2 } - ]) - - await Profile.insert([ - { id: 13, p_id: 3, user_id: 1 }, - { id: 14, p_id: 4, user_id: 2 } - ]) - - await Image.insert([ - { id: 15, i_id: 5, profile_id: 3 }, - { id: 16, i_id: 6, profile_id: 4 } - ]) - - const user = User.query() - .with('image') - .find(11) as User - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe(11) - expect(user.image).toBeInstanceOf(Image) - expect(user.image.profile_id).toBe(3) - expect(user.image.id).toBe(15) - }) -}) diff --git a/test/feature/relations/MorphMany_Persist.spec.ts b/test/feature/relations/MorphMany_Persist.spec.ts deleted file mode 100644 index 2e7ce1df..00000000 --- a/test/feature/relations/MorphMany_Persist.spec.ts +++ /dev/null @@ -1,749 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Morph Many – Persist', () => { - it('can create a data with morph many relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comments: [ - { - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: 3, - body: 'comment2', - commentable_id: 1, - commentable_type: 'posts' - } - ] - }) - - createState({ - posts: { - 1: { $id: '1', id: 1, comments: [] } - }, - videos: {}, - comments: { - 2: { - $id: '2', - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - 3: { - $id: '3', - id: 3, - body: 'comment2', - commentable_id: 1, - commentable_type: 'posts' - } - } - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['3'].commentable_type).toBe( - 'posts' - ) - }) - - it('can create many data with many morph many relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create([ - { - id: 1, - comments: [ - { - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: 3, - body: 'comment2', - commentable_id: 1, - commentable_type: 'posts' - } - ] - }, - { - id: 2, - comments: [ - { - id: 4, - body: 'comment3', - commentable_id: 2, - commentable_type: 'posts' - } - ] - } - ]) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['3'].commentable_type).toBe( - 'posts' - ) - expect(store.state.entities.comments.data['4'].id).toBe(4) - expect(store.state.entities.comments.data['4'].commentable_type).toBe( - 'posts' - ) - }) - - it('generates id and types for the morph many relations', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comments: [ - { id: 2, body: 'comment1' }, - { id: 3, body: 'comment2' } - ] - }) - - createState({ - posts: { - 1: { $id: '1', id: 1, comments: [] } - }, - videos: {}, - comments: { - 2: { - $id: '2', - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - 3: { - $id: '3', - id: 3, - body: 'comment2', - commentable_id: 1, - commentable_type: 'posts' - } - } - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['3'].commentable_type).toBe( - 'posts' - ) - }) - - it('generates only missing id or types for the morph many relations', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comments: [ - { id: 2, body: 'comment1', commentable_id: 1 }, - { id: 3, body: 'comment2', commentable_id: 2 } - ] - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['3'].commentable_type).toBe( - 'posts' - ) - }) - - it('would not generate id or types for non nested relations', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Comment.create([ - { id: 2, body: 'comment1' }, - { id: 3, body: 'comment2' } - ]) - - createState({ - posts: {}, - videos: {}, - comments: { - 2: { - $id: '2', - id: 2, - body: 'comment1', - commentable_id: null, - commentable_type: null - }, - 3: { - $id: '3', - id: 3, - body: 'comment2', - commentable_id: null, - commentable_type: null - } - } - }) - - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(null) - expect(store.state.entities.comments.data['2'].commentable_type).toBe(null) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(null) - expect(store.state.entities.comments.data['3'].commentable_type).toBe(null) - }) - - it('generates id or types for the deeply nested morph many relations', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr('null'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await User.create({ - id: 1, - posts: [ - { - id: 5, - user_id: 1, - comments: [ - { id: 2, body: 'comment1', commentable_id: 1 }, - { id: 3, body: 'comment2', commentable_id: 2 } - ] - } - ] - }) - - createState({ - users: { - 1: { $id: '1', id: 1, posts: [] } - }, - posts: { - 5: { $id: '5', id: 5, user_id: 1, comments: [] } - }, - videos: {}, - comments: { - 2: { - $id: '2', - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - 3: { - $id: '3', - id: 3, - body: 'comment2', - commentable_id: 2, - commentable_type: 'posts' - } - } - }) - - expect(store.state.entities.users.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['5'].id).toBe(5) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(1) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['3'].commentable_id).toBe(2) - expect(store.state.entities.comments.data['3'].commentable_type).toBe( - 'posts' - ) - }) - - it('can create morph many relation with value of null', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([{ model: Post }, { model: Comment }]) - - await Post.create({ id: 1, comments: null }) - - const expected = createState({ - posts: { - 1: { $id: '1', id: 1, comments: [] } - }, - comments: {} - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can resolve a morph many relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comment' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comments: [ - { - id: 1, - body: 'comment1', - commentabe_id: 1, - commentable_type: 'posts' - }, - { id: 2, body: 'comment2', commentabe_id: 1, commentable_type: 'posts' } - ] - }) - - const post = store.getters['entities/posts/query']() - .with('comments') - .find(1) - - expect(post).toBeInstanceOf(Post) - expect(post.comments.length).toBe(2) - expect(post.comments[0]).toBeInstanceOf(Comment) - expect(post.comments[1]).toBeInstanceOf(Comment) - }) - - it('generates id or type for the morph relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comment' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comments: [ - { id: 1, body: 'comment1' }, - { id: 2, body: 'comment2' } - ] - }) - - const post = store.getters['entities/posts/query']() - .with('comments') - .find(1) - - expect(post).toBeInstanceOf(Post) - expect(post.comments.length).toBe(2) - expect(post.comments[0]).toBeInstanceOf(Comment) - expect(post.comments[1]).toBeInstanceOf(Comment) - }) -}) diff --git a/test/feature/relations/MorphMany_Retrieve.spec.ts b/test/feature/relations/MorphMany_Retrieve.spec.ts deleted file mode 100644 index 88d585ab..00000000 --- a/test/feature/relations/MorphMany_Retrieve.spec.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Morph Many – Retrieve', () => { - it('can resolve morph many relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([{ id: 1 }, { id: 5 }, { id: 6 }]) - - await Video.create({ id: 3 }) - - await Comment.create([ - { - id: '1', - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: '2', - body: 'comment2', - commentable_id: 3, - commentable_type: 'videos' - }, - { - id: '3', - body: 'comment3', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: '4', - body: 'comment4', - commentable_id: 5, - commentable_type: 'posts' - } - ]) - - const post = Post.query() - .with('comments') - .find(1) as Post - - expect(post.comments.length).toBe(2) - expect(post.comments[0].body).toBe('comment1') - expect(post.comments[1].body).toBe('comment3') - - const postWithoutComments = Post.query() - .with('comments') - .find(6) as Post - - expect(postWithoutComments.comments.length).toBe(0) - }) - - it('can resolve morph many relation with custom primary key', async () => { - class Post extends Model { - static entity = 'posts' - - static primaryKey = 'post_id' - - // @Attribute - post_id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - post_id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([{ post_id: 1 }, { post_id: 5 }]) - - await Video.create({ id: 3 }) - - await Comment.create([ - { - id: '1', - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: '2', - body: 'comment2', - commentable_id: 3, - commentable_type: 'videos' - }, - { - id: '3', - body: 'comment3', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: '4', - body: 'comment4', - commentable_id: 5, - commentable_type: 'posts' - } - ]) - - const post = Post.query() - .with('comments') - .find(1) as Post - - expect(post.comments.length).toBe(2) - expect(post.comments[0].body).toBe('comment1') - expect(post.comments[1].body).toBe('comment3') - }) - - it('can resolve morph many relation with custom local key', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type', 'post_id') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type', - 'post_id' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([ - { id: 2, post_id: 1 }, - { id: 3, post_id: 5 } - ]) - - await Video.create([{ id: 3 }, { id: 4 }]) - - await Comment.create([ - { - id: '1', - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: '2', - body: 'comment2', - commentable_id: 3, - commentable_type: 'videos' - }, - { - id: '3', - body: 'comment3', - commentable_id: 1, - commentable_type: 'posts' - }, - { - id: '4', - body: 'comment4', - commentable_id: 5, - commentable_type: 'posts' - } - ]) - - const post = Post.query() - .with('comments') - .find(2) as Post - - expect(post.comments.length).toBe(2) - expect(post.comments[0].body).toBe('comment1') - expect(post.comments[1].body).toBe('comment3') - - const video = Video.query() - .with('comments') - .find(4) as Post - - expect(video.comments.length).toBe(0) - }) - - it('can apply `orderBy` constraint on nested relations', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.insert({ id: 1, comments: [{ id: 1 }, { id: 3 }, { id: 2 }] }) - - await Video.insert({ id: 1, comments: [{ id: 4 }, { id: 6 }, { id: 5 }] }) - - const post = Post.query() - .with('comments', (query) => { - query.orderBy('id', 'asc') - }) - .find(1) as Post - - expect(post.comments[0].id).toBe(1) - expect(post.comments[1].id).toBe(2) - expect(post.comments[2].id).toBe(3) - - const video = Video.query() - .with('comments', (query) => { - query.orderBy('id', 'desc') - }) - .find(1) as Post - - expect(video.comments[0].id).toBe(6) - expect(video.comments[1].id).toBe(5) - expect(video.comments[2].id).toBe(4) - }) -}) diff --git a/test/feature/relations/MorphOne_Persist.spec.ts b/test/feature/relations/MorphOne_Persist.spec.ts deleted file mode 100644 index c828ff07..00000000 --- a/test/feature/relations/MorphOne_Persist.spec.ts +++ /dev/null @@ -1,515 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Morph One – Persist', () => { - it('can create data containing the morph one relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - const store = createStore([{ model: Post }, { model: Comment }]) - - await Post.create({ - id: 1, - comment: { - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1 - } - }) - - const expected = createState({ - posts: { - 1: { $id: '1', id: 1, comment: null } - }, - comments: { - 1: { - $id: '1', - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1, - commentable: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('generates id and types for the morph one relations', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comment: { id: 2, body: 'comment1' } - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(1) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - }) - - it('generates only missing id or types for the morph one relations', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comment: { id: 2, body: 'comment1', commentable_id: 1 } - }) - - createState({ - posts: { - 1: { $id: '1', id: 1, comment: null } - }, - videos: {}, - comments: { - 2: { - $id: '2', - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - } - } - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(1) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - }) - - it('would not generate id or types for non nested relations', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comments: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Comment.create([ - { id: 2, body: 'comment1' }, - { id: 3, body: 'comment2' } - ]) - - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(null) - expect(store.state.entities.comments.data['2'].commentable_type).toBe(null) - expect(store.state.entities.comments.data['3'].id).toBe(3) - expect(store.state.entities.comments.data['3'].commentable_id).toBe(null) - expect(store.state.entities.comments.data['3'].commentable_type).toBe(null) - }) - - it('generates id or types for the deeply nested morph one relations', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr('null'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: User }, - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await User.create({ - id: 1, - posts: [ - { - id: 5, - user_id: 1, - comment: { id: 2, body: 'comment1', commentable_id: 1 } - } - ] - }) - - createState({ - users: { - 1: { $id: '1', id: 1, posts: [] } - }, - posts: { - 5: { $id: '5', id: 5, user_id: 1, comment: null } - }, - videos: {}, - comments: { - 2: { - $id: '2', - id: 2, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - } - } - }) - - expect(store.state.entities.users.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['5'].id).toBe(5) - expect(store.state.entities.comments.data['2'].id).toBe(2) - expect(store.state.entities.comments.data['2'].commentable_id).toBe(1) - expect(store.state.entities.comments.data['2'].commentable_type).toBe( - 'posts' - ) - }) - - it('can create data containing the empty morph one relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - const store = createStore([{ model: Post }, { model: Comment }]) - - await Post.create([ - { - id: 1, - comment: { - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1 - } - }, - { - id: 2, - comment: null - } - ]) - - const expected = createState({ - posts: { - 1: { $id: '1', id: 1, comment: null }, - 2: { $id: '2', id: 2, comment: null } - }, - comments: { - 1: { - $id: '1', - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1, - commentable: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can resolve a morph one relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comment: { - id: 1, - body: 'comment1', - commentabe_id: 1, - commentable_type: 'posts' - } - }) - - const post = store.getters['entities/posts/query']() - .with('comment') - .find(1) - - expect(post).toBeInstanceOf(Post) - expect(post.comment).toBeInstanceOf(Comment) - }) - - it('generates id or type for the morph relation', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Comment } - ]) - - await Post.create({ - id: 1, - comment: { id: 1, body: 'comment1' } - }) - - const post = store.getters['entities/posts/query']() - .with('comment') - .find(1) - - expect(post).toBeInstanceOf(Post) - expect(post.comment).toBeInstanceOf(Comment) - }) -}) diff --git a/test/feature/relations/MorphOne_Retrieve.spec.ts b/test/feature/relations/MorphOne_Retrieve.spec.ts deleted file mode 100644 index b1eeb6e1..00000000 --- a/test/feature/relations/MorphOne_Retrieve.spec.ts +++ /dev/null @@ -1,404 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Morph One – Retrieve', () => { - it('can resolve morph one relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([{ id: 1 }, { id: 5 }]) - - await Video.create({ id: 3 }) - - await Comment.create([ - { - $id: '1', - id: 1, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - $id: '2', - id: 2, - body: 'comment2', - commentable_id: 3, - commentable_type: 'videos' - }, - { - $id: '3', - id: 3, - body: 'comment3', - commentable_id: 2, - commentable_type: 'posts' - }, - { - $id: '4', - id: 4, - body: 'comment4', - commentable_id: 5, - commentable_type: 'posts' - } - ]) - - const post = Post.query() - .with('comment') - .find(1) as Post - - expect(post.comment.body).toBe('comment1') - }) - - it('can resolve empty morph one relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comments!: Comment | null - - static fields() { - return { - id: this.attr(null), - comments: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([{ id: 1 }, { id: 5 }]) - - await Video.create({ id: 3 }) - - const post = Post.query() - .with('comments') - .find(1) as Post - - expect(post.comments).toBe(null) - }) - - it('can resolve morph one relation with custom primary key', async () => { - class Post extends Model { - static entity = 'posts' - - static primaryKey = 'post_id' - - // @Attribute - post_id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - post_id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([{ post_id: 1 }, { post_id: 5 }]) - - await Video.create({ id: 3 }) - - await Comment.create([ - { - $id: '1', - id: 1, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - $id: '2', - id: 2, - body: 'comment2', - commentable_id: 3, - commentable_type: 'videos' - }, - { - $id: '3', - id: 3, - body: 'comment3', - commentable_id: 2, - commentable_type: 'posts' - }, - { - $id: '4', - id: 4, - body: 'comment4', - commentable_id: 5, - commentable_type: 'posts' - } - ]) - - const post = Post.query() - .with('comment') - .find(1) as Post - - expect(post.comment.body).toBe('comment1') - }) - - it('can resolve morph one relation with custom local key', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type', 'post_id') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null), - comment: this.morphOne( - Comment, - 'commentable_id', - 'commentable_type', - 'post_id' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.create([ - { id: 2, post_id: 1 }, - { id: 3, post_id: 5 } - ]) - - await Video.create({ id: 3 }) - - await Comment.create([ - { - $id: '1', - id: 1, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts' - }, - { - $id: '2', - id: 2, - body: 'comment2', - commentable_id: 3, - commentable_type: 'videos' - }, - { - $id: '3', - id: 3, - body: 'comment3', - commentable_id: 2, - commentable_type: 'posts' - }, - { - $id: '4', - id: 4, - body: 'comment4', - commentable_id: 5, - commentable_type: 'posts' - } - ]) - - const post = Post.query() - .with('comment') - .find(2) as Post - - expect(post.comment.body).toBe('comment1') - }) -}) diff --git a/test/feature/relations/MorphTo.spec.ts b/test/feature/relations/MorphTo.spec.ts deleted file mode 100644 index d9d75090..00000000 --- a/test/feature/relations/MorphTo.spec.ts +++ /dev/null @@ -1,270 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Morph To', () => { - it('can create data containing the morph to relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - // @MorphTo('commentable_id', 'commentable_type') - commentable!: Post - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - const store = createStore([{ model: Post }, { model: Comment }]) - - await Comment.create({ - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1, - commentable: { id: 1 } - }) - - const expected = createState({ - posts: { - 1: { $id: '1', id: 1, comment: null } - }, - comments: { - 1: { - $id: '1', - id: 1, - body: 'The Body', - commentable_type: 'posts', - commentable_id: 1, - commentable: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can retrieve all morph relations', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - // @MorphTo('commentable_id', 'commentable_type') - commentable!: Post | Video - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Post.insert({ - id: 1, - comments: [ - { id: 1, body: 'comment1' }, - { id: 2, body: 'comment2' } - ] - }) - - await Video.insert({ - id: 1, - comments: [ - { id: 3, body: 'comment3' }, - { id: 4, body: 'comment4' } - ] - }) - - const comments = Comment.query() - .with('commentable') - .get() - - expect(comments.length).toBe(4) - - expect(comments[0]).toBeInstanceOf(Comment) - expect(comments[0].commentable).toBeInstanceOf(Post) - - expect(comments[2]).toBeInstanceOf(Comment) - expect(comments[2].commentable).toBeInstanceOf(Video) - }) - - it('can resolve empty morph to relations', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - // @MorphTo('commentable_id', 'commentable_type') - commentable!: Post | Video - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - - await Comment.insert({ - id: 1, - body: 'Body 01' - }) - - const comment = Comment.query() - .with('commentable') - .find(1) as Comment - - expect(comment.commentable).toBe(null) - }) -}) diff --git a/test/feature/relations/MorphToMany_Persist.spec.ts b/test/feature/relations/MorphToMany_Persist.spec.ts deleted file mode 100644 index aab33f32..00000000 --- a/test/feature/relations/MorphToMany_Persist.spec.ts +++ /dev/null @@ -1,576 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Morph To Many – Persist', () => { - it('can create a morph to many relation data', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null), - public: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create({ - id: 1, - tags: [ - { id: 2, name: 'news', pivot: { public: true } }, - { id: 3, name: 'cast' } - ] - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.tags.data['2'].id).toBe(2) - expect(store.state.entities.tags.data['3'].id).toBe(3) - expect(store.state.entities.taggables.data['1_2_posts'].taggable_id).toBe(1) - expect(store.state.entities.taggables.data['1_2_posts'].taggable_type).toBe( - 'posts' - ) - expect(store.state.entities.taggables.data['1_2_posts'].public).toBe(true) - expect(store.state.entities.taggables.data['1_3_posts'].taggable_id).toBe(1) - expect(store.state.entities.taggables.data['1_3_posts'].taggable_type).toBe( - 'posts' - ) - expect(store.state.entities.taggables.data['1_3_posts'].public).toBe(null) - }) - - it('can create a morph to many data without relation field', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tag' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create({ id: 1 }) - - const expected = { - 1: { $id: '1', id: 1, tags: [] } - } - - expect(store.state.entities.posts.data).toEqual(expected) - }) - - it('can create a morph to many relation data with uid as id set on pivot model', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create({ - id: 1, - tags: [ - { id: 2, name: 'news' }, - { id: 3, name: 'cast' } - ] - }) - - const expected = createState({ - posts: { - 1: { $id: '1', id: 1, tags: [] } - }, - videos: {}, - tags: { - 2: { $id: '2', id: 2, name: 'news' }, - 3: { $id: '3', id: 3, name: 'cast' } - }, - taggables: { - '1_2_posts': { - $id: '1_2_posts', - tag_id: 2, - taggable_id: 1, - taggable_type: 'posts' - }, - '1_3_posts': { - $id: '1_3_posts', - tag_id: 3, - taggable_id: 1, - taggable_type: 'posts' - } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a morph to many relation data with uid as id set on parent model', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.uid(), - title: this.attr(''), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create({ - title: 'Post title.', - tags: [ - { id: 2, name: 'news' }, - { id: 3, name: 'cast' } - ] - }) - - const expected = createState({ - posts: { - $uid1: { $id: '$uid1', id: '$uid1', title: 'Post title.', tags: [] } - }, - videos: {}, - tags: { - 2: { $id: '2', id: 2, name: 'news' }, - 3: { $id: '3', id: 3, name: 'cast' } - }, - taggables: { - $uid1_2_posts: { - $id: '$uid1_2_posts', - id: null, - tag_id: 2, - taggable_id: '$uid1', - taggable_type: 'posts' - }, - $uid1_3_posts: { - $id: '$uid1_3_posts', - id: null, - tag_id: 3, - taggable_id: '$uid1', - taggable_type: 'posts' - } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can create a morph to many relation data with pivot data having custom key', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ).as('tag_pivot') - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null), - public: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create({ - id: 1, - tags: [ - { id: 2, name: 'news', tag_pivot: { public: true } }, - { id: 3, name: 'cast' } - ] - }) - - expect(store.state.entities.taggables.data['1_2_posts'].public).toBe(true) - expect(store.state.entities.taggables.data['1_3_posts'].public).toBe(null) - }) - - it('can resolve a morph to many relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tag' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([ - { - id: 1, - tags: [ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ] - }, - { - id: 2, - tags: [ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ] - } - ]) - - const post = Post.query() - .with('tags') - .find(1) as Post - - expect(post).toBeInstanceOf(Post) - expect(post.tags.length).toBe(2) - expect(post.tags[0]).toBeInstanceOf(Tag) - expect(post.tags[1]).toBeInstanceOf(Tag) - }) -}) diff --git a/test/feature/relations/MorphToMany_Retrieve.spec.ts b/test/feature/relations/MorphToMany_Retrieve.spec.ts deleted file mode 100644 index a957d14f..00000000 --- a/test/feature/relations/MorphToMany_Retrieve.spec.ts +++ /dev/null @@ -1,664 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Morph To Many – Retrieve', () => { - it('can resolve morph to many relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Intermediate - pivot!: Taggable - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - // @Attribute - is_public!: boolean - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null), - is_public: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([{ id: 1 }, { id: 5 }, { id: 6 }]) - - await Video.create({ id: 3 }) - - await Tag.create([ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ]) - - await Taggable.create([ - { - id: 1, - tag_id: 1, - taggable_id: 1, - taggable_type: 'posts', - is_public: true - }, - { - id: 2, - tag_id: 2, - taggable_id: 3, - taggable_type: 'videos', - is_public: true - }, - { - id: 3, - tag_id: 2, - taggable_id: 1, - taggable_type: 'posts', - is_public: false - } - ]) - - const post = Post.query() - .with('tags') - .find(1) as Post - - expect(post.tags.length).toBe(2) - expect(post.tags[0].name).toBe('news') - expect(post.tags[0].pivot.is_public).toBe(true) - expect(post.tags[1].name).toBe('cast') - expect(post.tags[1].pivot.is_public).toBe(false) - - const postWithoutTags = Post.query() - .with('tags') - .find(6) as Post - - expect(postWithoutTags.tags.length).toBe(0) - }) - - it('can resolve morph to many relation with custom primary key', async () => { - class Post extends Model { - static entity = 'posts' - - static primaryKey = 'post_id' - - // @Attribute - post_id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - post_id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([{ post_id: 1 }, { post_id: 5 }]) - - await Video.create({ id: 3 }) - - await Tag.create([ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ]) - - await Taggable.create([ - { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 2, taggable_id: 1, taggable_type: 'posts' } - ]) - - const post = Post.query() - .with('tags') - .find(1) as Post - - expect(post.tags.length).toBe(2) - expect(post.tags[0].name).toBe('news') - expect(post.tags[1].name).toBe('cast') - }) - - it('can resolve morph to many relation with custom local key', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type', 'post_id') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type', - 'post_id' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([ - { id: 1, post_id: 10 }, - { id: 5, post_id: 11 } - ]) - - await Video.create({ id: 3 }) - - await Tag.create([ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ]) - - await Taggable.create([ - { id: 1, tag_id: 1, taggable_id: 10, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 2, taggable_id: 10, taggable_type: 'posts' } - ]) - - const post = Post.query() - .with('tags') - .find(1) as Post - - expect(post.tags.length).toBe(2) - expect(post.tags[0].name).toBe('news') - expect(post.tags[1].name).toBe('cast') - }) - - it('can resolve morph to many relation with custom local and related key', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type', 'post_id', 'tag_id') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type', - 'post_id', - 'tag_id' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([ - { id: 1, post_id: 10 }, - { id: 5, post_id: 11 } - ]) - - await Video.create({ id: 3 }) - - await Tag.create([ - { id: 1, tag_id: 100, name: 'news' }, - { id: 2, tag_id: 101, name: 'cast' } - ]) - - await Taggable.create([ - { id: 1, tag_id: 100, taggable_id: 10, taggable_type: 'posts' }, - { id: 2, tag_id: 100, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 101, taggable_id: 10, taggable_type: 'posts' } - ]) - - const post = Post.query() - .with('tags') - .find(1) as Post - - expect(post.tags.length).toBe(2) - expect(post.tags[0].name).toBe('news') - expect(post.tags[1].name).toBe('cast') - }) - - it('can apply `orderBy` constraint on nested relations', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.insert({ - id: 1, - tags: [{ id: 1 }, { id: 3 }, { id: 2 }] - }) - - await Video.insert({ - id: 1, - tags: [{ id: 1 }, { id: 3 }, { id: 2 }] - }) - - const post = Post.query() - .with('tags', (query) => { - query.orderBy('id', 'asc') - }) - .find(1) as Post - - expect(post.tags[0].id).toBe(1) - expect(post.tags[1].id).toBe(2) - expect(post.tags[2].id).toBe(3) - - const video = Video.query() - .with('tags', (query) => { - query.orderBy('id', 'desc') - }) - .find(1) as Video - - expect(video.tags[0].id).toBe(3) - expect(video.tags[1].id).toBe(2) - expect(video.tags[2].id).toBe(1) - }) -}) diff --git a/test/feature/relations/MorphedByMany_Persist.spec.ts b/test/feature/relations/MorphedByMany_Persist.spec.ts deleted file mode 100644 index 7f8a08f8..00000000 --- a/test/feature/relations/MorphedByMany_Persist.spec.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Morphed By Many – Persist', () => { - it('can create a morphed by many relation data', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null), - public: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Tag.create({ - id: 1, - name: 'news', - posts: [{ id: 1, pivot: { public: true } }, { id: 2 }], - videos: [{ id: 3, pivot: { public: true } }, { id: 4 }] - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.videos.data['3'].id).toBe(3) - expect(store.state.entities.videos.data['4'].id).toBe(4) - expect(store.state.entities.tags.data['1'].id).toBe(1) - expect(store.state.entities.taggables.data['1_1_posts'].taggable_id).toBe(1) - expect(store.state.entities.taggables.data['1_1_posts'].taggable_type).toBe( - 'posts' - ) - expect(store.state.entities.taggables.data['1_1_posts'].public).toBe(true) - expect(store.state.entities.taggables.data['2_1_posts'].taggable_id).toBe(2) - expect(store.state.entities.taggables.data['2_1_posts'].taggable_type).toBe( - 'posts' - ) - expect(store.state.entities.taggables.data['2_1_posts'].public).toBe(null) - expect(store.state.entities.taggables.data['3_1_videos'].taggable_id).toBe( - 3 - ) - expect( - store.state.entities.taggables.data['3_1_videos'].taggable_type - ).toBe('videos') - expect(store.state.entities.taggables.data['3_1_videos'].public).toBe(true) - expect(store.state.entities.taggables.data['4_1_videos'].taggable_id).toBe( - 4 - ) - expect( - store.state.entities.taggables.data['4_1_videos'].taggable_type - ).toBe('videos') - }) - - it('can create many morphed by many relation data', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Tag.create([ - { - id: 1, - name: 'news', - posts: [{ id: 1 }, { id: 2 }], - videos: [{ id: 3 }, { id: 4 }] - }, - { - id: 2, - name: 'cast', - posts: [{ id: 2 }, { id: 3 }], - videos: [{ id: 3 }, { id: 5 }] - } - ]) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.posts.data['3'].id).toBe(3) - - expect(store.state.entities.videos.data['3'].id).toBe(3) - expect(store.state.entities.videos.data['4'].id).toBe(4) - expect(store.state.entities.videos.data['5'].id).toBe(5) - - expect(store.state.entities.tags.data['1'].id).toBe(1) - expect(store.state.entities.tags.data['2'].id).toBe(2) - - expect(store.state.entities.taggables.data['1_1_posts'].taggable_id).toBe(1) - expect(store.state.entities.taggables.data['1_1_posts'].taggable_type).toBe( - 'posts' - ) - - expect(store.state.entities.taggables.data['2_1_posts'].taggable_id).toBe(2) - expect(store.state.entities.taggables.data['2_1_posts'].taggable_type).toBe( - 'posts' - ) - - expect(store.state.entities.taggables.data['3_1_videos'].taggable_id).toBe( - 3 - ) - expect( - store.state.entities.taggables.data['3_1_videos'].taggable_type - ).toBe('videos') - - expect(store.state.entities.taggables.data['4_1_videos'].taggable_id).toBe( - 4 - ) - expect( - store.state.entities.taggables.data['4_1_videos'].taggable_type - ).toBe('videos') - - expect(store.state.entities.taggables.data['2_2_posts'].taggable_id).toBe(2) - expect(store.state.entities.taggables.data['2_2_posts'].taggable_type).toBe( - 'posts' - ) - - expect(store.state.entities.taggables.data['3_2_posts'].taggable_id).toBe(3) - expect(store.state.entities.taggables.data['3_2_posts'].taggable_type).toBe( - 'posts' - ) - - expect(store.state.entities.taggables.data['3_2_videos'].taggable_id).toBe( - 3 - ) - expect( - store.state.entities.taggables.data['3_2_videos'].taggable_type - ).toBe('videos') - - expect(store.state.entities.taggables.data['5_2_videos'].taggable_id).toBe( - 5 - ) - expect( - store.state.entities.taggables.data['5_2_videos'].taggable_type - ).toBe('videos') - }) - - it('can create a morphed by many relation data with pivot data having custom key', async () => { - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ).as('tag'), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ).as('tag') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null), - public: this.attr(null) - } - } - } - - const store = createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Tag.create({ - id: 1, - name: 'news', - posts: [{ id: 1, tag: { public: true } }, { id: 2 }], - videos: [{ id: 3, tag: { public: true } }, { id: 4 }] - }) - - expect(store.state.entities.posts.data['1'].id).toBe(1) - expect(store.state.entities.posts.data['2'].id).toBe(2) - expect(store.state.entities.videos.data['3'].id).toBe(3) - expect(store.state.entities.videos.data['4'].id).toBe(4) - expect(store.state.entities.tags.data['1'].id).toBe(1) - expect(store.state.entities.taggables.data['1_1_posts'].taggable_id).toBe(1) - expect(store.state.entities.taggables.data['1_1_posts'].taggable_type).toBe( - 'posts' - ) - expect(store.state.entities.taggables.data['1_1_posts'].public).toBe(true) - expect(store.state.entities.taggables.data['2_1_posts'].taggable_id).toBe(2) - expect(store.state.entities.taggables.data['2_1_posts'].taggable_type).toBe( - 'posts' - ) - expect(store.state.entities.taggables.data['2_1_posts'].public).toBe(null) - expect(store.state.entities.taggables.data['3_1_videos'].taggable_id).toBe( - 3 - ) - expect( - store.state.entities.taggables.data['3_1_videos'].taggable_type - ).toBe('videos') - expect(store.state.entities.taggables.data['3_1_videos'].public).toBe(true) - expect(store.state.entities.taggables.data['4_1_videos'].taggable_id).toBe( - 4 - ) - expect( - store.state.entities.taggables.data['4_1_videos'].taggable_type - ).toBe('videos') - }) -}) diff --git a/test/feature/relations/MorphedByMany_Retrieve.spec.ts b/test/feature/relations/MorphedByMany_Retrieve.spec.ts deleted file mode 100644 index 613ffcb6..00000000 --- a/test/feature/relations/MorphedByMany_Retrieve.spec.ts +++ /dev/null @@ -1,541 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Morphed By Many – Retrieve', () => { - it('can resolve morphed by many relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Intermediate - pivot!: Taggable - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @Intermediate - pivot!: Taggable - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @MorphedByMany(Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - posts!: Post[] - - // @MorphedByMany(Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - videos!: Video[] - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - // @Attribute - is_public!: boolean - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null), - is_public: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([{ id: 1 }, { id: 5 }]) - - await Video.create([{ id: 3 }, { id: 4 }, { id: 5 }]) - - await Tag.create([ - { id: 1, name: 'news' }, - { id: 3, name: 'without references' } - ]) - - await Taggable.create([ - { - id: 1, - tag_id: 1, - taggable_id: 1, - taggable_type: 'posts', - is_public: false - }, - { - id: 2, - tag_id: 2, - taggable_id: 3, - taggable_type: 'videos', - is_public: true - }, - { - id: 3, - tag_id: 1, - taggable_id: 5, - taggable_type: 'posts', - is_public: true - }, - { - id: 4, - tag_id: 1, - taggable_id: 4, - taggable_type: 'videos', - is_public: false - } - ]) - - const tag = Tag.query() - .with('posts') - .with('videos') - .find(1) as Tag - - expect(tag.posts.length).toBe(2) - expect(tag.posts[0].pivot.is_public).toBe(false) - expect(tag.posts[1].pivot.is_public).toBe(true) - expect(tag.videos.length).toBe(1) - expect(tag.videos[0].pivot.is_public).toBe(false) - - const tagWithoutReferences = Tag.query() - .with('posts') - .with('videos') - .find(3) as Tag - - expect(tagWithoutReferences.videos.length).toBe(0) - expect(tagWithoutReferences.posts.length).toBe(0) - }) - - it('can resolve morphed by many relation with custom primary key', async () => { - class Post extends Model { - static entity = 'posts' - - static primaryKey = 'post_id' - - // @Attribute - post_id!: number - - static fields() { - return { - post_id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - static primaryKey = 'tag_id' - - // @Attribute - tag_id!: number - - // @Attribute('') - name!: string - - // @MorphedByMany(Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - posts!: Post[] - - // @MorphedByMany(Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - videos!: Video[] - - static fields() { - return { - tag_id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([{ post_id: 1 }, { post_id: 5 }]) - - await Video.create([{ id: 3 }, { id: 4 }, { id: 5 }]) - - await Tag.create({ tag_id: 1, name: 'news' }) - - await Taggable.create([ - { id: 1, tag_id: 1, taggable_id: 1, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 1, taggable_id: 5, taggable_type: 'posts' }, - { id: 4, tag_id: 1, taggable_id: 4, taggable_type: 'videos' } - ]) - - const tag = Tag.query() - .with('posts') - .with('videos') - .find(1) as Tag - - expect(tag.posts.length).toBe(2) - expect(tag.videos.length).toBe(1) - }) - - it('can resolve morphed by many relation with custom local and related key', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute('') - name!: string - - // @MorphedByMany(Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type', 'tag_id', 'post_id') - posts!: Post[] - - // @MorphedByMany(Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type', 'tag_id') - videos!: Video[] - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type', - 'tag_id', - 'post_id' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type', - 'tag_id' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Post.create([ - { id: 1, post_id: 100 }, - { id: 5, post_id: 105 } - ]) - - await Video.create([{ id: 3 }, { id: 4 }, { id: 5 }]) - - await Tag.create({ id: 1, tag_id: 200, name: 'news' }) - - await Taggable.create([ - { id: 1, tag_id: 200, taggable_id: 100, taggable_type: 'posts' }, - { id: 2, tag_id: 2, taggable_id: 3, taggable_type: 'videos' }, - { id: 3, tag_id: 200, taggable_id: 105, taggable_type: 'posts' }, - { id: 4, tag_id: 200, taggable_id: 4, taggable_type: 'videos' } - ]) - - const tag = Tag.query() - .with('posts') - .with('videos') - .find(1) as Tag - - expect(tag.posts.length).toBe(2) - expect(tag.videos.length).toBe(1) - }) - - it('can apply `orderBy` constraint on nested relations', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @MorphedByMany(Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - posts!: Post[] - - // @MorphedByMany(Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - videos!: Video[] - - static fields() { - return { - id: this.attr(null), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - // @Attribute - id!: number - - // @Attribute - tag_id!: number - - // @Attribute - taggable_id!: number - - // @Attribute - taggable_type!: string - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - await Tag.create({ - id: 1, - posts: [{ id: 1 }, { id: 3 }, { id: 2 }], - videos: [{ id: 1 }, { id: 3 }, { id: 2 }] - }) - - const tag = Tag.query() - .with('posts', (query) => { - query.orderBy('id', 'asc') - }) - .with('videos', (query) => { - query.orderBy('id', 'desc') - }) - .find(1) as Tag - - expect(tag.posts[0].id).toBe(1) - expect(tag.posts[1].id).toBe(2) - expect(tag.posts[2].id).toBe(3) - - expect(tag.videos[0].id).toBe(3) - expect(tag.videos[1].id).toBe(2) - expect(tag.videos[2].id).toBe(1) - }) -}) diff --git a/test/feature/relations/Persist_NestedMorph.spec.ts b/test/feature/relations/Persist_NestedMorph.spec.ts deleted file mode 100644 index 4c9b0061..00000000 --- a/test/feature/relations/Persist_NestedMorph.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Features – Relations – Persist – Nested Morph', () => { - it('can create data with nested morph one and morph to relationship', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - comments: this.hasMany(Comment, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - const store = createStore([ - { model: User }, - { model: Post }, - { model: Comment } - ]) - - await User.create({ - id: 1, - comments: [ - { - id: 1, - user_id: 1, - commentable_id: 1, - commentable_type: 'posts', - commentable: { id: 1 } - } - ] - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, comments: [] } - }, - posts: { - 1: { $id: '1', id: 1 } - }, - comments: { - 1: { - $id: '1', - id: 1, - user_id: 1, - commentable_type: 'posts', - commentable_id: 1, - commentable: null - } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/relations/Retrieve_Constraint.spec.ts b/test/feature/relations/Retrieve_Constraint.spec.ts deleted file mode 100644 index 6379b717..00000000 --- a/test/feature/relations/Retrieve_Constraint.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Relations – Retrieve – Constraint', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id') - } - } - } - - it('can resolve relation constraint', async () => { - const store = createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 1 }, - { id: 3, user_id: 2 } - ]) - - const expected = { - $id: '1', - id: 1, - posts: [{ $id: '1', id: 1, user_id: 1, user: null }] - } - - const user = store.getters['entities/users/query']() - .with('posts', (query: Query) => { - query.where('id', 1) - }) - .first() - - expect(user).toEqual(expected) - }) -}) diff --git a/test/feature/relations/Retrieve_DeepLoad.spec.ts b/test/feature/relations/Retrieve_DeepLoad.spec.ts deleted file mode 100644 index bb8f4d3d..00000000 --- a/test/feature/relations/Retrieve_DeepLoad.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Relations – Deep Load', () => { - /** - * This test is a regression test for issue #152. It try's to see that the - * user attached to the comment should be `null`. - * See: https://github.com/vuex-orm/vuex-orm/issues/152 - */ - it('can retrieve deep nested morphMany relation', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany( - Comment, - 'commentable_id', - 'commentable_type' - ) - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - commentable_id: this.attr(null), - commentable_type: this.attr(''), - user: this.belongsTo(User, 'user_id') - } - } - } - - createStore([{ model: User }, { model: Post }, { model: Comment }]) - - await Post.create({ - id: 1, - comments: [ - { - id: 1, - user_id: 1, - user: { - id: 1, - name: 'John Doe' - } - } - ] - }) - - const post = Post.query() - .withAll() - .first() - - const expected = { - $id: '1', - id: 1, - comments: [ - { - $id: '1', - id: 1, - user_id: 1, - commentable_id: 1, - commentable_type: 'posts', - user: null - } - ] - } - - expect(post).toEqual(expected) - }) -}) diff --git a/test/feature/relations/Retrieve_Has.spec.ts b/test/feature/relations/Retrieve_Has.spec.ts deleted file mode 100644 index 42567652..00000000 --- a/test/feature/relations/Retrieve_Has.spec.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Feature – Relations – Retrieve – Has', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id') - } - } - } - - it('can query data depending on relationship existence', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - const expected = [ - { $id: '1', id: 1, posts: [] }, - { $id: '2', id: 2, posts: [] } - ] - - const users = User.query() - .has('posts') - .get() - - expect(users).toEqual(expected) - }) - - it('can query data depending on relationship absence', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - const expected = [{ $id: '3', id: 3, posts: [] }] - - const users = User.query() - .hasNot('posts') - .get() - - expect(users).toEqual(expected) - }) - - it('can query data depending on relationship existence with constraint', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Phone }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - await Phone.create([{ id: 1, user_id: 2 }]) - - const expected1 = [{ $id: '1', id: 1, phone: null, posts: [] }] - const expected2 = [{ $id: '2', id: 2, phone: null, posts: [] }] - - const users1 = User.query() - .whereHas('posts', (query: Query) => { - query.where('id', 1) - }) - .get() - - const users2 = User.query() - .whereHas('phone', (query: Query) => { - query.where('id', 1) - }) - .get() - - expect(users1).toEqual(expected1) - expect(users2).toEqual(expected2) - }) - - it('can query data depending on relationship absence with constraint', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - const expected = [ - { $id: '2', id: 2, posts: [] }, - { $id: '3', id: 3, posts: [] } - ] - - const users = User.query() - .whereHasNot('posts', (query: Query) => { - query.where('id', 1) - }) - .get() - - expect(users).toEqual(expected) - }) - - it('can query data depending on relationship existence with number', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - const expected = [{ $id: '2', id: 2, posts: [] }] - - const users = User.query() - .has('posts', 2) - .get() - - expect(users).toEqual(expected) - }) - - it('can query data depending on relationship absence with number', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - const expected = [ - { $id: '1', id: 1, posts: [] }, - { $id: '3', id: 3, posts: [] } - ] - - const users = User.query() - .hasNot('posts', 2) - .get() - - expect(users).toEqual(expected) - }) - - it('can query data depending on relationship existence with string conditions', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Phone }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - await Phone.create([{ id: 1, user_id: 1 }]) - - expect( - User.query() - .has('posts', 1) - .get() - ).toEqual([ - { $id: '1', id: 1, phone: null, posts: [] }, - { $id: '2', id: 2, phone: null, posts: [] } - ]) - expect( - User.query() - .has('posts', '=', 1) - .get() - ).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - expect( - User.query() - .has('posts', '>', 1) - .get() - ).toEqual([{ $id: '2', id: 2, phone: null, posts: [] }]) - expect( - User.query() - .has('posts', '>=', 1) - .get() - ).toEqual([ - { $id: '1', id: 1, phone: null, posts: [] }, - { $id: '2', id: 2, phone: null, posts: [] } - ]) - expect( - User.query() - .has('posts', '<', 2) - .get() - ).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - expect( - User.query() - .has('posts', '<=', 2) - .get() - ).toEqual([ - { $id: '1', id: 1, phone: null, posts: [] }, - { $id: '2', id: 2, phone: null, posts: [] } - ]) - expect( - User.query() - .has('posts', 'unknown', 1) - .get() - ).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - - expect( - User.query() - .has('phone', '=', 1) - .get() - ).toEqual([{ $id: '1', id: 1, phone: null, posts: [] }]) - }) - - it('can query data depending on relationship absence with string conditions', async () => { - createStore([{ model: User }, { model: Post }]) - - await User.create([{ id: 1 }, { id: 2 }, { id: 3 }]) - - await Post.create([ - { id: 1, user_id: 1 }, - { id: 2, user_id: 2 }, - { id: 3, user_id: 2 } - ]) - - expect( - User.query() - .hasNot('posts', '>', 1) - .get() - ).toEqual([ - { $id: '1', id: 1, posts: [] }, - { $id: '3', id: 3, posts: [] } - ]) - expect( - User.query() - .hasNot('posts', '>=', 1) - .get() - ).toEqual([{ $id: '3', id: 3, posts: [] }]) - expect( - User.query() - .hasNot('posts', '<', 2) - .get() - ).toEqual([ - { $id: '2', id: 2, posts: [] }, - { $id: '3', id: 3, posts: [] } - ]) - expect( - User.query() - .hasNot('posts', '<=', 2) - .get() - ).toEqual([{ $id: '3', id: 3, posts: [] }]) - }) -}) diff --git a/test/feature/relations/Retrieve_SelfRelation.spec.ts b/test/feature/relations/Retrieve_SelfRelation.spec.ts deleted file mode 100644 index a5ed0e5d..00000000 --- a/test/feature/relations/Retrieve_SelfRelation.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Retrieve – Self Relation', () => { - it('can resolve self relation', async () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - parent_id!: number - - // @BelongsTo(Post, 'parent_id') - parent!: Post - - static fields() { - return { - id: this.attr(null), - parent_id: this.attr(null), - parent: this.belongsTo(Post, 'parent_id') - } - } - } - - createStore([{ model: Post }]) - - await Post.insert({ id: 1, parent_id: null }) - await Post.insert({ id: 2, parent_id: 1 }) - - const post = Post.query() - .with('parent') - .find(2) as Post - - expect(post.parent).toBeInstanceOf(Post) - }) -}) diff --git a/test/feature/relations/Retrieve_StringDefine.spec.ts b/test/feature/relations/Retrieve_StringDefine.spec.ts deleted file mode 100644 index 9f810713..00000000 --- a/test/feature/relations/Retrieve_StringDefine.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations - String Define', () => { - it('can define related model via string', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany('posts', 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany('posts', 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await Post.create({ id: 2, user_id: 1 }) - - const user = User.query() - .with('posts') - .find(1) as User - - expect(user).toBeInstanceOf(User) - expect(user.id).toBe(1) - expect(user.posts[0]).toBeInstanceOf(Post) - expect(user.posts[0].id).toBe(2) - }) -}) diff --git a/test/feature/relations/Retrieve_With.spec.ts b/test/feature/relations/Retrieve_With.spec.ts deleted file mode 100644 index 14c615a9..00000000 --- a/test/feature/relations/Retrieve_With.spec.ts +++ /dev/null @@ -1,951 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – Retrieve – With', () => { - it('can resolve all relations', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasOne(Phone, 'user_id') - phone!: Phone - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Phone }, { model: Post }]) - - await User.create({ - id: 1, - phone: { id: 2, user_id: 1 }, - posts: [ - { id: 3, user_id: 1 }, - { id: 4, user_id: 1 } - ] - }) - - const user1 = User.query() - .with('*') - .first() as User - - expect(user1.phone.id).toBe(2) - expect(user1.posts[0].id).toBe(3) - expect(user1.posts[1].id).toBe(4) - - const user2 = User.query() - .withAll() - .first() as User - - expect(user2.phone.id).toBe(2) - expect(user2.posts[0].id).toBe(3) - expect(user2.posts[1].id).toBe(4) - }) - - it('can resolve all relations recursively', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasOne(Phone, 'user_id') - phone!: Phone - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @BelongsTo(User, 'user_id') - user!: User - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id') - } - } - } - - createStore([{ model: User }, { model: Phone }, { model: Post }]) - - await User.create({ - id: 1, - phone: { id: 2, user_id: 1 }, - posts: [ - { - id: 3, - user_id: 1, - user: { id: 1 } - }, - { - id: 4, - user_id: 1, - user: { id: 1 } - } - ] - }) - - const user1 = User.query() - .withAllRecursive() - .first() as User - - expect(user1.phone.id).toBe(2) - expect(user1.posts[0].id).toBe(3) - expect(user1.posts[0].user.id).toBe(1) - expect(user1.posts[1].id).toBe(4) - expect(user1.posts[1].user.id).toBe(1) - - const user2 = User.query() - .withAllRecursive(0) - .first() as User - - expect(user2.phone.id).toBe(2) - expect(user2.posts[0].id).toBe(3) - expect(user2.posts[0].user).toBe(null) - expect(user2.posts[1].id).toBe(4) - expect(user2.posts[1].user).toBe(null) - }) - - it('can resolve child relation', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @HasMany(Comment, 'post_id') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.hasMany(Comment, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }, { model: Comment }]) - - await User.create({ - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ] - } - ] - }) - - const expected = { - $id: '1', - id: 1, - posts: [ - { - $id: '1', - id: 1, - user_id: 1, - comments: [ - { $id: '1', id: 1, post_id: 1 }, - { $id: '2', id: 2, post_id: 1 } - ] - }, - { - $id: '2', - id: 2, - user_id: 1, - comments: [ - { $id: '3', id: 3, post_id: 2 }, - { $id: '4', id: 4, post_id: 2 } - ] - } - ] - } - - const users = User.query() - .with('posts.comments') - .find(1) - - expect(users).toEqual(expected) - }) - - it('can resolve child relation for multiple records', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @HasMany(Comment, 'post_id') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.hasMany(Comment, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }, { model: Comment }]) - - await User.create([ - { - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - } - ] - }, - { - id: 2 - } - ]) - - const expected = [ - { - $id: '1', - id: 1, - posts: [ - { - $id: '1', - id: 1, - user_id: 1, - comments: [ - { $id: '1', id: 1, post_id: 1 }, - { $id: '2', id: 2, post_id: 1 } - ] - } - ] - }, - { - $id: '2', - id: 2, - posts: [] - } - ] - - const users = User.query() - .with('posts.comments') - .findIn([1, 2]) - - expect(users).toEqual(expected) - }) - - it('can resolve even deeper child relation', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @HasMany(Comment, 'post_id') - posts!: Comment[] - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.hasMany(Comment, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - // @HasMany(Like, 'comment_id') - likes!: Comment[] - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null), - likes: this.hasMany(Like, 'comment_id') - } - } - } - - class Like extends Model { - static entity = 'likes' - - // @Attribute - id!: number - - // @Attribute - comment_id!: number - - static fields() { - return { - id: this.attr(null), - comment_id: this.attr(null) - } - } - } - - createStore([ - { model: User }, - { model: Post }, - { model: Comment }, - { model: Like } - ]) - - await User.create({ - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1, likes: [{ id: 1, comment_id: 1 }] }, - { id: 2, post_id: 1, likes: [{ id: 2, comment_id: 2 }] } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2, likes: [{ id: 3, comment_id: 3 }] }, - { id: 4, post_id: 2, likes: [{ id: 4, comment_id: 4 }] } - ] - } - ] - }) - - const expected = { - $id: '1', - id: 1, - posts: [ - { - $id: '1', - id: 1, - user_id: 1, - comments: [ - { - $id: '1', - id: 1, - post_id: 1, - likes: [{ $id: '1', id: 1, comment_id: 1 }] - }, - { - $id: '2', - id: 2, - post_id: 1, - likes: [{ $id: '2', id: 2, comment_id: 2 }] - } - ] - }, - { - $id: '2', - id: 2, - user_id: 1, - comments: [ - { - $id: '3', - id: 3, - post_id: 2, - likes: [{ $id: '3', id: 3, comment_id: 3 }] - }, - { - $id: '4', - id: 4, - post_id: 2, - likes: [{ $id: '4', id: 4, comment_id: 4 }] - } - ] - } - ] - } - - const users = User.query() - .with('posts.comments.likes') - .find(1) - - expect(users).toEqual(expected) - }) - - it('can resolve child relations with multiple sub relations with pipe', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @HasMany(Comment, 'post_id') - comments!: Comment[] - - // @HasMany(Like, 'post_id') - likes!: Like[] - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.hasMany(Comment, 'post_id'), - likes: this.hasMany(Like, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - class Like extends Model { - static entity = 'likes' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - createStore([ - { model: User }, - { model: Post }, - { model: Comment }, - { model: Like } - ]) - - await User.create({ - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ], - likes: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ], - likes: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ] - } - ] - }) - - const expected = { - $id: '1', - id: 1, - posts: [ - { - $id: '1', - id: 1, - user_id: 1, - comments: [ - { $id: '1', id: 1, post_id: 1 }, - { $id: '2', id: 2, post_id: 1 } - ], - likes: [ - { $id: '1', id: 1, post_id: 1 }, - { $id: '2', id: 2, post_id: 1 } - ] - }, - { - $id: '2', - id: 2, - user_id: 1, - comments: [ - { $id: '3', id: 3, post_id: 2 }, - { $id: '4', id: 4, post_id: 2 } - ], - likes: [ - { $id: '3', id: 3, post_id: 2 }, - { $id: '4', id: 4, post_id: 2 } - ] - } - ] - } - - const user1 = User.query() - .with('posts.comments|likes') - .find(1) - const user2 = User.query() - .with('posts.*') - .find(1) - - expect(user1).toEqual(expected) - expect(user2).toEqual(expected) - }) - - it('can resolve child relations with multiple sub relations in array', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.hasMany(Comment, 'post_id'), - likes: this.hasMany(Like, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - class Like extends Model { - static entity = 'likes' - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - createStore([ - { model: User }, - { model: Post }, - { model: Comment }, - { model: Like } - ]) - - await User.create({ - id: 1, - posts: [ - { - id: 1, - user_id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ], - likes: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }, - { - id: 2, - user_id: 1, - comments: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ], - likes: [ - { id: 3, post_id: 2 }, - { id: 4, post_id: 2 } - ] - } - ] - }) - - const expected = { - $id: '1', - id: 1, - posts: [ - { - $id: '1', - id: 1, - user_id: 1, - comments: [ - { $id: '1', id: 1, post_id: 1 }, - { $id: '2', id: 2, post_id: 1 } - ], - likes: [ - { $id: '1', id: 1, post_id: 1 }, - { $id: '2', id: 2, post_id: 1 } - ] - }, - { - $id: '2', - id: 2, - user_id: 1, - comments: [ - { $id: '3', id: 3, post_id: 2 }, - { $id: '4', id: 4, post_id: 2 } - ], - likes: [ - { $id: '3', id: 3, post_id: 2 }, - { $id: '4', id: 4, post_id: 2 } - ] - } - ] - } - - const user1 = User.query() - .with(['posts.comments', 'posts.likes']) - .find(1) - const user2 = User.query() - .with(['posts.*']) - .find(1) - - expect(user1).toEqual(expected) - expect(user2).toEqual(expected) - }) - - it('ignores any unknown relationship name', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.create({ - id: 1, - posts: [{ id: 1, user_id: 1 }] - }) - - const user = User.query() - .with('unknown') - .first() as User - - expect(user.posts.length).toBe(0) - }) - - it('does not retrieve empty relation', async () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @BelongsTo(User, 'user_id') - author!: User - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - author: this.belongsTo(User, 'user_id') - } - } - } - - createStore([{ model: User }, { model: Post }]) - - await User.create({ id: 1 }) - - await Post.create({ id: 1, user_id: null }) - - const post = Post.query() - .with('*') - .first() as Post - - expect(post.id).toEqual(1) - expect(post.user_id).toEqual(null) - expect(post.author).toEqual(null) - }) -}) diff --git a/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts b/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts deleted file mode 100644 index 9a9a25d6..00000000 --- a/test/feature/relations/Retrieve_With_NestedConstraints.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Relations – With – Nested Constraints', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasMany(Post, 'user_id') - posts!: Post[] - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @HasMany(Comment, 'post_id') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - comments: this.hasMany(Comment, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute('') - title!: string - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - title: this.attr(''), - post_id: this.attr(null) - } - } - } - - beforeEach(() => { - createStore([{ model: User }, { model: Post }, { model: Comment }]) - }) - - it('can apply `with` constraints to nested relations', async () => { - await User.create({ id: 1 }) - - await Post.create({ id: 1, user_id: 1 }) - - await Comment.create([ - { id: 1, post_id: 1, title: 'Title01' }, - { id: 2, post_id: 1, title: 'Title01' }, - { id: 3, post_id: 1, title: 'Title02' } - ]) - - const user = User.query() - .with('posts.comments', (query) => { - query.where('title', 'Title01') - }) - .find(1) as User - - expect(user.posts.length).toBe(1) - expect(user.posts[0].comments.length).toBe(2) - expect(user.posts[0].comments[0].title).toBe('Title01') - }) - - it('can apply `withAll` constraints to nested relations', async () => { - await User.create({ data: { id: 1 } }) - - await Post.create({ - data: [ - { id: 1, user_id: 1 }, - { id: 2, user_id: 1 } - ] - }) - - await Comment.create({ - data: [ - { id: 1, post_id: 1, title: 'Title01' }, - { id: 2, post_id: 1, title: 'Title01' }, - { id: 3, post_id: 1, title: 'Title02' } - ] - }) - - const user = User.query() - .withAll((query) => { - query.where('id', 2) - }) - .find(1) as User - - expect(user.posts.length).toBe(1) - expect(user.posts[0].comments.length).toEqual(0) - expect(user.posts[0].id).toBe(2) - }) - - it('can apply constraints to multiple sub relations', async () => { - await User.create({ data: { id: 1 } }) - - await Post.create({ data: { id: 1, user_id: 1 } }) - - await Comment.create({ - data: [ - { id: 1, post_id: 1, title: 'Beta' }, - { id: 2, post_id: 1, title: 'Cyprus' }, - { id: 3, post_id: 1, title: 'Alpha' } - ] - }) - - const user = User.query() - .with('posts.*', (query) => { - query.orderBy('title') - }) - .find(1) as User - - const expected = [ - { $id: '3', id: 3, title: 'Alpha', post_id: 1 }, - { $id: '1', id: 1, title: 'Beta', post_id: 1 }, - { $id: '2', id: 2, title: 'Cyprus', post_id: 1 } - ] - - expect(user.posts.length).toBe(1) - expect(user.posts[0].comments.length).toBe(3) - expect(user.posts[0].comments).toEqual(expected) - }) -}) diff --git a/test/feature/relations/WhereFk.spec.ts b/test/feature/relations/WhereFk.spec.ts deleted file mode 100644 index d7874597..00000000 --- a/test/feature/relations/WhereFk.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Feature – Retrieve – Where', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @Attribute - age!: number - - // @Attribute(false) - active!: boolean - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - age: this.attr(null), - active: this.attr(false) - } - } - - isActive() { - return this.active - } - } - - it('can handle previously filled _joinedIdFilter', async () => { - createStore([{ model: User }]) - - await User.create({ data: [] }) - - const query = User.query() - - query.whereFk('id', [1]) - query.whereFk('id', [1, 2]) - - const keys = Array.from((query as any).joinedIdFilter.values()) - - expect(keys).toEqual([1]) - }) -}) diff --git a/test/feature/repositories/Crud_Delete.spec.ts b/test/feature/repositories/Crud_Delete.spec.ts deleted file mode 100644 index 82d80a08..00000000 --- a/test/feature/repositories/Crud_Delete.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model } from '@/index' - -describe('Feature - Repositories - CRUD Delete', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - - id!: any - } - - it('can delete a record by specifying the id', async () => { - const store = createStore([User]) - - const user = store.$repo(User) - - await user.insert({ - data: [{ id: 1 }, { id: 2 }] - }) - - await user.delete(1) - - const expected = createState({ - users: { - 2: { $id: '2', id: 2 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete records by specifying a closure', async () => { - const store = createStore([User]) - - const user = store.$repo(User) - - await user.insert({ - data: [{ id: 1 }, { id: 2 }] - }) - - await user.delete((user) => user.id === 2) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1 } - } - }) - - expect(store.state.entities).toEqual(expected) - }) - - it('can delete all records in the entity', async () => { - const store = createStore([User]) - - const user = store.$repo(User) - - await user.insert({ - data: [{ id: 1 }, { id: 2 }] - }) - - await user.deleteAll() - - const expected = createState({ - users: {} - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/repositories/Crud_InsertOrUpdate.spec.ts b/test/feature/repositories/Crud_InsertOrUpdate.spec.ts deleted file mode 100644 index 5d9caabd..00000000 --- a/test/feature/repositories/Crud_InsertOrUpdate.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model } from '@/index' - -describe('Feature - Repositories - CRUD Insert or Update', () => { - it('can insert or update given record to the store', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - const store = createStore([User]) - - await store.$repo(User).insert({ - data: { id: 1, name: 'John Doe' } - }) - - await store.$repo(User).insertOrUpdate({ - data: [ - { id: 1, name: 'Jane Doe' }, - { id: 2, name: 'Johnny Doe' } - ] - }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' }, - 2: { $id: '2', id: 2, name: 'Johnny Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/repositories/Crud_Retrieve.spec.ts b/test/feature/repositories/Crud_Retrieve.spec.ts deleted file mode 100644 index 8aead697..00000000 --- a/test/feature/repositories/Crud_Retrieve.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import { Model, Query } from '@/index' - -describe('Feature - Repositories - CRUD Retrieve', () => { - it('can get a new query instance', () => { - class User extends Model { - static entity = 'users' - } - - const store = createStore([User]) - - const query = store.$repo(User).query() - - expect(query).toBeInstanceOf(Query) - }) - - it('can get all records', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - - id!: number - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - await userRepo.insert({ - data: [{ id: 1 }, { id: 2 }] - }) - - const users = userRepo.all() - - expect(users.length).toBe(2) - expect(users[0].id).toBe(1) - expect(users[1].id).toBe(2) - }) - - it('can find a single record', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - - id!: number - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - await userRepo.insert({ - data: [{ id: 1 }, { id: 2 }] - }) - - const user = userRepo.find(2) as User - - expect(user.id).toBe(2) - }) - - it('can find a single record with composite primary key', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['idA', 'idB'] - - static fields() { - return { - idA: this.attr(null), - idB: this.attr(null) - } - } - - idA!: number - idB!: number - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - await userRepo.insert({ - data: [ - { idA: 1, idB: 2 }, - { idA: 3, idB: 4 } - ] - }) - - const user = userRepo.find([1, 2]) as User - - expect(user.idA).toBe(1) - expect(user.idB).toBe(2) - }) - - it('can find records by ids', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - - id!: number - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - await userRepo.insert({ - data: [{ id: 1 }, { id: 2 }, { id: 3 }] - }) - - const users = userRepo.findIn([1, 3]) - - expect(users[0].id).toBe(1) - expect(users[1].id).toBe(3) - }) - - it('can find records by ids with composite primary key', async () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['idA', 'idB'] - - static fields() { - return { - idA: this.attr(null), - idB: this.attr(null) - } - } - - idA!: number - idB!: number - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - await userRepo.insert({ - data: [ - { idA: 1, idB: 2 }, - { idA: 3, idB: 4 }, - { idA: 5, idB: 6 } - ] - }) - - const users = userRepo.findIn([ - [1, 2], - [5, 6] - ]) - - expect(users[0].idA).toBe(1) - expect(users[0].idB).toBe(2) - expect(users[1].idA).toBe(5) - expect(users[1].idB).toBe(6) - }) - - it('can check whether record exists', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - expect(userRepo.exists()).toBe(false) - - await userRepo.insert({ - data: { id: 1 } - }) - - expect(userRepo.exists()).toBe(true) - }) -}) diff --git a/test/feature/repositories/Crud_Update.spec.ts b/test/feature/repositories/Crud_Update.spec.ts deleted file mode 100644 index 5a7d9e86..00000000 --- a/test/feature/repositories/Crud_Update.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createStore, createState } from 'test/support/Helpers' -import { Model } from '@/index' - -describe('Feature - Repositories - CRUD Update', () => { - it('can update record by including primary key in the data', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.string('') - } - } - } - - const store = createStore([User]) - - const user = store.$repo(User) - - await user.insert({ - data: { id: 1, name: 'John Doe' } - }) - - await user.update({ id: 1, name: 'Jane Doe' }) - - const expected = createState({ - users: { - 1: { $id: '1', id: 1, name: 'Jane Doe' } - } - }) - - expect(store.state.entities).toEqual(expected) - }) -}) diff --git a/test/feature/repositories/Repository.spec.ts b/test/feature/repositories/Repository.spec.ts deleted file mode 100644 index ca500171..00000000 --- a/test/feature/repositories/Repository.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Repository from '@/repository/Repository' -import Model from '@/model/Model' - -describe('Feature - Repository', () => { - it('can retrieve a new repository instance from the store', () => { - class User extends Model { - static entity = 'users' - } - - const store = createStore([User]) - - const user = store.$repo(User) - - expect(user).toBeInstanceOf(Repository) - }) - - it('can instantiate a new model instance without a record', () => { - class User extends Model { - static entity = 'users' - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - expect(userRepo.make()).toBeInstanceOf(User) - }) - - it('can instantiate a new model instance with a record', () => { - class User extends Model { - static entity = 'users' - - // @Attribute('') - name!: string - - // @Attribute('') - email!: string - - static fields() { - return { - id: this.attr(''), - name: this.attr('') - } - } - } - - const store = createStore([User]) - - const userRepo = store.$repo(User) - - const user = userRepo.make({ name: 'John Doe' }) - - expect(user).toBeInstanceOf(User) - expect(user.name).toBe('John Doe') - }) - - it('injects store instance to the instantiated model.', () => { - class User extends Model { - static entity = 'users' - } - - const store = createStore([User]) - - const user = store.$repo(User).make() - - expect(user.$store()).toBe(store) - }) - - it('can retrieve a custom repository', () => { - class User extends Model { - static entity = 'users' - } - - class UserRepository extends Repository { - model = User - foo = 'bar' - } - - const store = createStore([User]) - - const userRepo = store.$repo(UserRepository) - - expect(userRepo.foo).toBe('bar') - }) - - it('throws if the custom repository has no assigned model', () => { - class User extends Model { - static entity = 'users' - } - - class UserRepository extends Repository { - foo = 'bar' - } - - const store = createStore([User]) - - expect(() => store.$repo(UserRepository)).toThrow() - }) -}) diff --git a/test/performance/Retrieve_BelongsTo.spec.ts b/test/performance/Retrieve_BelongsTo.spec.ts deleted file mode 100644 index f563bdb5..00000000 --- a/test/performance/Retrieve_BelongsTo.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Performance – Retrieve – Belongs To', () => { - it('should retrieve belongsTo relation in time', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - user: this.belongsTo(User, 'user_id') - } - } - } - - const users = [] - const posts = [] - - for (let i = 1; i <= 30000; i++) { - users.push({ id: i }) - } - - posts.push({ id: 1, user_id: 1 }) - posts.push({ id: 2, user_id: 1 }) - posts.push({ id: 3, user_id: 2 }) - posts.push({ id: 4, user_id: 2 }) - posts.push({ id: 5, user_id: 3 }) - posts.push({ id: 6, user_id: 3 }) - posts.push({ id: 7, user_id: 4 }) - posts.push({ id: 8, user_id: 4 }) - - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/users/create', { data: users }) - await store.dispatch('entities/posts/create', { data: posts }) - - const start = +new Date() - - store.getters['entities/posts/query']() - .with('user') - .get() - - const end = +new Date() - - expect(end - start).toBeLessThan(500) - - console.info('\x1b[2m%s\x1b[0m', ` -- The test took ${end - start}ms`) - }) -}) diff --git a/test/performance/Retrieve_BelongsToMany.spec.ts b/test/performance/Retrieve_BelongsToMany.spec.ts deleted file mode 100644 index 2fc4003e..00000000 --- a/test/performance/Retrieve_BelongsToMany.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Performance – Retrieve – Belongs To Many', () => { - it('should retrieve belongsToMany relation in time', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - static fields() { - return { - id: this.attr(null), - roleType: this.attr('') - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static primaryKey = ['role_id', 'user_id'] - - static fields() { - return { - role_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const users = [] - const roles = [] - const roleUser = [] - - for (let i = 1; i <= 2000; i++) { - users.push({ id: i }) - } - - for (let i = 1; i <= 2000; i++) { - roles.push({ id: i }) - } - - for (let i = 1; i <= 2000; i++) { - roleUser.push({ user_id: i, role_id: i }) - } - - const store = createStore([ - { model: User }, - { model: Role }, - { model: RoleUser } - ]) - - await store.dispatch('entities/users/create', { data: users }) - await store.dispatch('entities/roles/create', { data: roles }) - await store.dispatch('entities/roleUser/create', { data: roleUser }) - - const start = +new Date() - - store.getters['entities/users/query']() - .with('roles') - .get() - - const end = +new Date() - - expect(end - start).toBeLessThan(300) - console.info('\x1b[2m%s\x1b[0m', ` -- The test took ${end - start}ms`) - }) -}) diff --git a/test/performance/Retrieve_HasMany.spec.ts b/test/performance/Retrieve_HasMany.spec.ts deleted file mode 100644 index 12311f50..00000000 --- a/test/performance/Retrieve_HasMany.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Performance – Retrieve – Has Many', () => { - it('should retrieve hasMany relation in time', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const users = [] - const posts = [] - - for (let i = 1; i <= 2000; i++) { - users.push({ id: i }) - } - - for (let i = 1; i <= 2000; i++) { - posts.push({ id: i }) - } - - const store = createStore([{ model: User }, { model: Post }]) - - await store.dispatch('entities/users/create', { data: users }) - await store.dispatch('entities/posts/create', { data: posts }) - - const start = +new Date() - - store.getters['entities/users/query']() - .with('posts') - .get() - - const end = +new Date() - - expect(end - start).toBeLessThan(300) - console.info('\x1b[2m%s\x1b[0m', ` -- The test took ${end - start}ms`) - }) -}) diff --git a/test/performance/Retrieve_HasManyThrough.spec.ts b/test/performance/Retrieve_HasManyThrough.spec.ts deleted file mode 100644 index d57671af..00000000 --- a/test/performance/Retrieve_HasManyThrough.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Performance – Retrieve – Has Many Through', () => { - it('should retrieve hasManyThrough relation in time', async () => { - class Country extends Model { - static entity = 'countries' - - static fields() { - return { - id: this.attr(null), - posts: this.hasManyThrough(Post, User, 'country_id', 'user_id') - } - } - } - - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - country_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - const countries = [] - const users = [] - const posts = [] - - for (let i = 1; i <= 2; i++) { - countries.push({ id: i }) - } - - // 100 users - for (let i = 1; i <= 100; i++) { - const countryID = (i % 2) + 1 - users.push({ id: i, country_id: countryID }) - } - - // 1 user 100 posts - for (let i = 1; i <= users.length; i++) { - for (let j = 1; j <= 100; j++) { - posts.push({ id: `${i}_${j}`, user_id: i }) - } - } - - const store = createStore([ - { model: Country }, - { model: User }, - { model: Post } - ]) - - await store.dispatch('entities/countries/create', { data: countries }) - await store.dispatch('entities/users/create', { data: users }) - await store.dispatch('entities/posts/create', { data: posts }) - - const start = +new Date() - - store.getters['entities/countries/query']() - .with('posts') - .get(1) - - const end = +new Date() - - expect(end - start).toBeLessThan(500) - console.info('\x1b[2m%s\x1b[0m', ` -- The test took ${end - start}ms`) - }) -}) diff --git a/test/performance/Retrieve_HasOneThrough.spec.ts b/test/performance/Retrieve_HasOneThrough.spec.ts deleted file mode 100644 index 22879cad..00000000 --- a/test/performance/Retrieve_HasOneThrough.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Performance – Retrieve – Has One Through', () => { - it('should retrieve relation in time', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - image: this.hasOneThrough(Image, Profile, 'user_id', 'profile_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Image extends Model { - static entity = 'images' - - static fields() { - return { - id: this.attr(null), - profile_id: this.attr(null) - } - } - } - - const users = [] - const profiles = [] - const images = [] - - for (let i = 1; i <= 1000; i++) { - users.push({ id: i }) - } - - for (let i = 1; i <= 1000; i++) { - profiles.push({ id: i, user_id: i }) - } - - for (let i = 1; i <= 1000; i++) { - images.push({ id: i, profile_id: i }) - } - - const store = createStore([ - { model: User }, - { model: Profile }, - { model: Image } - ]) - - await store.dispatch('entities/users/create', { data: users }) - await store.dispatch('entities/profiles/create', { data: profiles }) - await store.dispatch('entities/images/create', { data: images }) - - const start = +new Date() - - store.getters['entities/users/query']() - .with('image') - .get() - - const end = +new Date() - - expect(end - start).toBeLessThan(300) - console.info('\x1b[2m%s\x1b[0m', ` -- The test took ${end - start}ms`) - }) -}) diff --git a/test/support/Helpers.ts b/test/support/Helpers.ts deleted file mode 100644 index c0995207..00000000 --- a/test/support/Helpers.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Vue from 'vue' -import Vuex, { Store, Module } from 'vuex' -import VuexORM from '@/index' -import { mapValues } from '@/support/Utils' -import { Record, Collection } from '@/data' -import Database from '@/database/Database' -import Model from '@/model/Model' -import State from '@/modules/contracts/State' - -Vue.use(Vuex) - -export interface Entities { - model: typeof Model - module?: Module -} - -/** - * Check if the given model is model object. - */ -function isModel(model: any): model is typeof Model { - return model.prototype !== undefined -} - -/** - * Create a new store instance. - */ -export function createStore( - entities: Entities[] | typeof Model[], - namespace: string = 'entities' -): Store { - const database = new Database() - - entities.forEach((entity: Entities | typeof Model) => { - isModel(entity) - ? database.register(entity) - : database.register(entity.model, entity.module) - }) - - return new Store({ - plugins: [VuexORM.install(database, { namespace })], - strict: true - }) -} - -/** - * Create a new store instance from the given database. - */ -export function createStoreFromDatabase(database: Database): Store { - return new Store({ - plugins: [VuexORM.install(database)], - strict: true - }) -} - -/** - * Fill store data with the given state. - */ -export function fillState(store: Store, state: Record): void { - for (const entity in state) { - store.commit('entities/$mutate', { - entity, - callback(s: State) { - s.data = state[entity] - } - }) - } -} - -/** - * Create a new entity state. - */ -export function createState(state: Record, namespace: string = 'entities') { - return { - $name: namespace, - ...mapValues(state, (data, name) => ({ - $connection: namespace, - $name: name, - data - })) - } -} - -/** - * Serialize given collection by invoking `$getAttributes` on each model - * in the given collection. - */ -export function serializeCollection(collection: Collection): Record[] { - return collection.map((m) => m.$getAttributes()) -} - -export default { - createStore, - createStoreFromDatabase, - fillState, - createState, - serializeCollection -} diff --git a/test/unit/VuexORM.spec.ts b/test/unit/VuexORM.spec.ts deleted file mode 100644 index 9df31009..00000000 --- a/test/unit/VuexORM.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import Vue from 'vue' -import Vuex from 'vuex' -import VuexORM from '@/index' -import Database from '@/database/Database' -import Model from '@/model/Model' - -describe('unit/VuexORM', () => { - Vue.use(Vuex) - - it('installs Vuex ORM to the store', () => { - class User extends Model { - static entity = 'users' - } - class Post extends Model { - static entity = 'posts' - } - - const database = new Database() - - database.register(User) - database.register(Post) - - const store = new Vuex.Store({ - plugins: [VuexORM.install(database)] - }) - - const expected = { - entities: { - $name: 'entities', - users: { - $name: 'users', - $connection: 'entities', - data: {} - }, - posts: { - $name: 'posts', - $connection: 'entities', - data: {} - } - } - } - - expect(store.state).toEqual(expected) - }) - - it('can customize the namespace', () => { - class User extends Model { - static entity = 'users' - } - - const database = new Database() - - database.register(User) - - const store = new Vuex.Store({ - plugins: [ - VuexORM.install(database, { - namespace: 'database' - }) - ] - }) - - const expected = { - database: { - $name: 'database', - users: { - $name: 'users', - $connection: 'database', - data: {} - } - } - } - - expect(store.state).toEqual(expected) - }) -}) diff --git a/test/unit/database/Database.spec.ts b/test/unit/database/Database.spec.ts deleted file mode 100644 index f2d79514..00000000 --- a/test/unit/database/Database.spec.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { createStoreFromDatabase } from 'test/support/Helpers' -import Database from '@/database/Database' -import Model from '@/model/Model' - -describe('Unit – Database', () => { - it('can fetch all models', () => { - class User extends Model { - static entity = 'users' - } - class Post extends Model { - static entity = 'posts' - } - - const database = new Database() - - database.register(User) - database.register(Post) - - const models = database.models() - - expect(models.users).toBe(User) - expect(models.posts).toBe(Post) - }) - - it('can fetch a model by string', () => { - class User extends Model { - static entity = 'users' - } - class Post extends Model { - static entity = 'posts' - } - - const database = new Database() - - database.register(User) - database.register(Post) - - const model = database.model('users') - - expect(model).toBe(User) - }) - - it('can fetch a model by type', () => { - class User extends Model { - static entity = 'users' - } - class Post extends Model { - static entity = 'posts' - } - - const database = new Database() - - database.register(User) - database.register(Post) - - const model = database.model(User) - - expect(model).toBe(User) - }) - - it('throws when a model is not found', () => { - class User extends Model { - static entity = 'users' - } - - const database = new Database() - - database.register(User) - - expect(() => database.model('posts')).toThrowError('[Vuex ORM]') - }) - - it('can fetch all base models', () => { - // Suppress model inheritance warning. - jest.spyOn(global.console, 'warn').mockImplementation() - - class User extends Model { - static entity = 'users' - } - class Guest extends Model { - static entity = 'guests' - static baseEntity = 'users' - } - class Admin extends Model { - static entity = 'admins' - static baseEntity = 'users' - } - - const database = new Database() - - database.register(User) - database.register(Guest) - database.register(Admin) - - const models = database.baseModels() - - expect(models.users).toBe(User) - expect(models.guests).toBe(User) - expect(models.admins).toBe(User) - }) - - it('can fetch a base model by string', () => { - // Suppress model inheritance warning. - jest.spyOn(global.console, 'warn').mockImplementation() - - class User extends Model { - static entity = 'users' - } - class Guest extends Model { - static entity = 'guests' - static baseEntity = 'users' - } - - const database = new Database() - - database.register(User) - database.register(Guest) - - const model = database.baseModel('guests') - - expect(model).toBe(User) - }) - - it('can fetch a base model by type', () => { - // Suppress model inheritance warning. - jest.spyOn(global.console, 'warn').mockImplementation() - - class User extends Model { - static entity = 'users' - } - class Guest extends Model { - static entity = 'guests' - static baseEntity = 'users' - } - - const database = new Database() - - database.register(User) - database.register(Guest) - - const model = database.baseModel(Guest) - - expect(model).toBe(User) - }) - - it('throws when a base model is not found', () => { - // Suppress model inheritance warning. - jest.spyOn(global.console, 'warn').mockImplementation() - - class User extends Model { - static entity = 'users' - } - class Guest extends Model { - static entity = 'guests' - static baseEntity = 'users' - } - - const database = new Database() - - database.register(User) - database.register(Guest) - - expect(() => database.baseModel('admins')).toThrowError('[Vuex ORM]') - }) - - it('can fetch all modules', () => { - class User extends Model { - static entity = 'users' - } - class Post extends Model { - static entity = 'posts' - } - - const users = {} - const posts = {} - - const database = new Database() - - database.register(User, users) - database.register(Post, posts) - - const modules = database.modules() - - expect(modules.users).toBe(users) - expect(modules.posts).toBe(posts) - }) - - it('can fetch a module', () => { - class User extends Model { - static entity = 'users' - } - class Post extends Model { - static entity = 'posts' - } - - const users = {} - const posts = {} - - const database = new Database() - - database.register(User, users) - database.register(Post, posts) - - const module = database.module('users') - - expect(module).toBe(users) - }) - - it('throws when a module is not found', () => { - class User extends Model { - static entity = 'users' - } - - const users = {} - - const database = new Database() - - database.register(User, users) - - expect(() => database.module('posts')).toThrowError('[Vuex ORM]') - }) - - it('binds the store instance to the registered models.', () => { - jest.spyOn(global.console, 'warn').mockImplementation() - - class User extends Model { - static entity = 'users' - } - - const database1 = new Database() - const database2 = new Database() - - database1.register(User) - database2.register(User) - - const store1 = createStoreFromDatabase(database1) - const store2 = createStoreFromDatabase(database2) - - expect( - store1 - .$db() - .model('users') - .database() - ).toBe(database1) - expect( - store2 - .$db() - .model('users') - .database() - ).toBe(database2) - }) - - it('warns when using the deprecated `$db` method', () => { - const warn = jest.spyOn(global.console, 'warn').mockImplementation() - - const database = new Database() - - const store = createStoreFromDatabase(database) - - store.$db() - - expect(warn).toBeCalled() - }) -}) diff --git a/test/unit/model/Model.spec.ts b/test/unit/model/Model.spec.ts deleted file mode 100644 index 634217fe..00000000 --- a/test/unit/model/Model.spec.ts +++ /dev/null @@ -1,298 +0,0 @@ -import Uid from '@/support/Uid' -import Model from '@/model/Model' - -describe('Unit – Model', () => { - beforeEach(() => { - Uid.reset() - }) - - it('can fetch empty fields when model fields is not declared', () => { - class User extends Model { - static entity = 'users' - } - - expect(User.fields()).toEqual({}) - }) - - it('should set default field values as a property on instantiation', () => { - class User extends Model { - static entity = 'users' - - // @Attribute('John Doe') - name!: string - - // @Attribute('john@example.com') - email!: string - - static fields() { - return { - name: this.attr('John Doe'), - email: this.attr('john@example.com') - } - } - } - - const user = new User() - - expect(user.name).toBe('John Doe') - expect(user.email).toBe('john@example.com') - }) - - it('should set default field values using a closure', () => { - let counter = 0 - - class User extends Model { - static entity = 'users' - - // @Attribute(() => counter++) - id!: number - - static fields() { - return { - id: this.attr(() => counter++) - } - } - } - - const user1 = new User() - const user2 = new User() - - expect(user1.id).toBe(0) - expect(user2.id).toBe(1) - }) - - it('should set given field values as a property on instantiation', () => { - class User extends Model { - static entity = 'users' - - // @Attribute('John Doe') - name!: string - - // @Attribute('john@example.com') - email!: string - - static fields() { - return { - name: this.attr('John Doe'), - email: this.attr('john@example.com') - } - } - } - - const user = new User({ name: 'Jane Doe', age: 32 }) - - expect(user.name).toBe('Jane Doe') - expect(user.email).toBe('john@example.com') - expect((user as any).age).toBe(undefined) - }) - - it('can get a value of the primary key', () => { - class User extends Model { - static fields() { - return { - id: this.attr(null) - } - } - } - - expect(User.getIndexIdFromRecord({ id: 1 })).toBe('1') - }) - - it('can get a value of the composite primary key', () => { - class Vote extends Model { - static primaryKey = ['vote_id', 'user_id'] - - static fields() { - return { - vote_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - expect(Vote.getIndexIdFromRecord({ user_id: 1, vote_id: 2 })).toBe('[2,1]') - }) - - it('can get local key of the model', () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['name', 'email'] - - static fields() { - return { - name: this.attr('John Doe'), - email: this.attr('john@example.com') - } - } - } - - expect(User.localKey()).toBe('id') - }) - - it('can get generate missing primary keys', () => { - class User extends Model { - static entity = 'users' - - static primaryKey = ['id', 'name', 'email'] - - static fields() { - return { - id: this.uid(), - name: this.attr(null), - email: this.attr(undefined) - } - } - } - - const user = new User().$generatePrimaryId() - - expect(user.$toJson()).toEqual({ - id: '$uid1', - name: '$uid2', - email: '$uid3' - }) - }) - - it('should return right model when getting a model from a record if the record is in a hierarchy', () => { - class User extends Model { - static entity = 'users' - - static typeKey = 'type' - - static types() { - return { - USER: User, - SUPER: SuperUser - } - } - } - - class SuperUser extends User { - static entity = 'super_users' - - static baseEntity = 'users' - } - - const result1 = User.getModelFromRecord({ type: 'USER' }) - expect(result1?.entity).toBe('users') - - const result2 = User.getModelFromRecord({ type: 'SUPER' }) - expect(result2?.entity).toBe('super_users') - }) - - it('should return null when getting a model from a record if model was not found', () => { - class User extends Model { - static entity = 'users' - - static typeKey = 'type' - - static types() { - return { - USER: User, - SUPER: SuperUser - } - } - } - - class SuperUser extends User { - static entity = 'superusers' - - static baseEntity = 'users' - } - - const result1 = User.getModelFromRecord({ type: 'UNKNOWN' }) - expect(result1).toBe(null) - - const result2 = User.getModelFromRecord({ data: 'bla' }) - expect(result2).toBe(null) - }) - - it('should return model when getting a model from a record if the record is already a model instance', () => { - class User extends Model { - static entity = 'users' - } - - const user = new User() - - const result = User.getModelFromRecord(user) - - expect(result).toBe(User) - }) - - it('throws error when trying to fetch attribute class that does not exist', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - name: this.attr('John Doe'), - email: this.attr('john@example.com') - } - } - } - - const shouldThrow = () => { - ;(User as any).getAttributeClass('blah') - } - - expect(shouldThrow).toThrow() - }) - - it('can hydrate given record', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('Default Doe') - } - } - } - - const record = User.hydrate({ id: 1, age: 24 }) - - expect(record).toEqual({ $id: null, id: 1, name: 'Default Doe' }) - }) - - it('can hydrate without passing any record', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('Default Doe') - } - } - } - - const record = User.hydrate() - - expect(record).toEqual({ $id: null, id: null, name: 'Default Doe' }) - }) - - it('should warn user that increment is deprecated', () => { - const spy = jest.spyOn(global.console, 'warn').mockImplementation() - - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string - - static fields() { - return { - // tslint:disable-next-line:deprecation - id: this.increment() - } - } - } - - const user = new User() - - expect(user.id).toBe('$uid1') - expect(spy).toHaveBeenCalled() - }) -}) diff --git a/test/unit/model/Model_Attributes_Boolean.spec.ts b/test/unit/model/Model_Attributes_Boolean.spec.ts deleted file mode 100644 index 1699605e..00000000 --- a/test/unit/model/Model_Attributes_Boolean.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Model from '@/model/Model' - -describe('Feature – Attributes – Boolean', () => { - it('casts the value to `Boolean` when instantiating the model', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Bool(true) - bool!: boolean - - static fields() { - return { - id: this.attr(null), - bool: this.boolean(true) - } - } - } - - expect(new User({}).bool).toBe(true) - expect(new User({ bool: '' }).bool).toBe(false) - expect(new User({ bool: 'string' }).bool).toBe(true) - expect(new User({ bool: '0' }).bool).toBe(false) - expect(new User({ bool: 0 }).bool).toBe(false) - expect(new User({ bool: 1 }).bool).toBe(true) - expect(new User({ bool: true }).bool).toBe(true) - expect(new User({ bool: null }).bool).toBe(false) - }) - - it('can mutate the value by specifying mutator at attribute', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Bool(true, { mutator: (value: any) => value }) - bool!: boolean - - static fields() { - return { - id: this.attr(null), - bool: this.boolean(true, (value: any) => !value) - } - } - } - - expect(new User({}).bool).toBe(false) - expect(new User({ bool: '' }).bool).toBe(true) - expect(new User({ bool: 'string' }).bool).toBe(false) - expect(new User({ bool: '0' }).bool).toBe(true) - expect(new User({ bool: 0 }).bool).toBe(true) - expect(new User({ bool: 1 }).bool).toBe(false) - expect(new User({ bool: true }).bool).toBe(false) - expect(new User({ bool: null }).bool).toBe(true) - }) - - it('can mutate the value by specifying mutator at mutators', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Bool(true) - bool!: boolean - - static fields() { - return { - id: this.attr(null), - bool: this.boolean(true) - } - } - - static mutators() { - return { - bool(value: any) { - return !value - } - } - } - } - - expect(new User({}).bool).toBe(false) - expect(new User({ bool: '' }).bool).toBe(true) - expect(new User({ bool: 'string' }).bool).toBe(false) - expect(new User({ bool: '0' }).bool).toBe(true) - expect(new User({ bool: 0 }).bool).toBe(true) - expect(new User({ bool: 1 }).bool).toBe(false) - expect(new User({ bool: true }).bool).toBe(false) - expect(new User({ bool: null }).bool).toBe(true) - }) -}) diff --git a/test/unit/model/Model_Attributes_Mutators.spec.ts b/test/unit/model/Model_Attributes_Mutators.spec.ts deleted file mode 100644 index 20c42671..00000000 --- a/test/unit/model/Model_Attributes_Mutators.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import Model from '@/model/Model' - -describe('Unit – Model', () => { - it('should mutate data if closure was given to the attr when instantiating', () => { - class User extends Model { - static entity = 'users' - - // @Attribute('', { mutator: (value: any) => value.toUpperCase() }) - name!: string - - static fields() { - return { - name: this.attr('', (value) => value.toUpperCase()) - } - } - } - - expect(new User({ name: 'john doe' }).name).toBe('JOHN DOE') - }) - - it('should mutate data if mutators are present', () => { - class User extends Model { - static entity = 'users' - - // @Attribute('') - name!: string - - static fields() { - return { - name: this.attr('') - } - } - - static mutators() { - return { - name(value: any) { - return value.toUpperCase() - } - } - } - } - - expect(new User({ name: 'john doe' }).name).toBe('JOHN DOE') - }) - - it('attr mutator should take precedence over static mutators', () => { - class User extends Model { - static entity = 'users' - - // @Attribute('', { mutator: (value: any) => value.toUpperCase() }) - name!: string - - static fields() { - return { - name: this.attr('', (value) => value.toUpperCase()) - } - } - - static mutators() { - return { - name(_value: any) { - return 'Not Expected' - } - } - } - } - - expect(new User({ name: 'john doe' }).name).toBe('JOHN DOE') - }) -}) diff --git a/test/unit/model/Model_Attributes_Number.spec.ts b/test/unit/model/Model_Attributes_Number.spec.ts deleted file mode 100644 index a2f630c7..00000000 --- a/test/unit/model/Model_Attributes_Number.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import Model from '@/model/Model' - -describe('Feature – Attributes – Number', () => { - it('casts the value to `Number` when instantiating the model', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Num(0) - num!: number - - static fields() { - return { - id: this.attr(null), - num: this.number(0) - } - } - } - - expect(new User({}).num).toBe(0) - expect(new User({ num: 1 }).num).toBe(1) - expect(new User({ num: '2' }).num).toBe(2) - expect(new User({ num: true }).num).toBe(1) - expect(new User({ num: false }).num).toBe(0) - expect(new User({ num: null }).num).toBe(0) - }) - - it('can mutate the value by specifying mutator at attribute', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Num(0) - num!: number - - static fields() { - return { - id: this.attr(null), - num: this.number(0, (value: any) => value + 1) - } - } - } - - expect(new User({}).num).toBe(1) - expect(new User({ num: 1 }).num).toBe(2) - expect(new User({ num: '2' }).num).toBe(3) - expect(new User({ num: true }).num).toBe(2) - expect(new User({ num: false }).num).toBe(1) - expect(new User({ num: null }).num).toBe(1) - }) - - it('can mutate the value by specifying mutator at mutators', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Num(0) - num!: number - - static fields() { - return { - id: this.attr(null), - num: this.number(0) - } - } - - static mutators() { - return { - num(value: any) { - return value + 1 - } - } - } - } - - expect(new User({}).num).toBe(1) - expect(new User({ num: 1 }).num).toBe(2) - expect(new User({ num: '2' }).num).toBe(3) - expect(new User({ num: true }).num).toBe(2) - expect(new User({ num: false }).num).toBe(1) - expect(new User({ num: null }).num).toBe(1) - }) -}) diff --git a/test/unit/model/Model_Attributes_String.spec.ts b/test/unit/model/Model_Attributes_String.spec.ts deleted file mode 100644 index 4626bb3b..00000000 --- a/test/unit/model/Model_Attributes_String.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Model from '@/model/Model' - -describe('Feature – Attributes – String', () => { - it('casts the value to `String` when instantiating the model', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Str('default') - str!: string - - static fields() { - return { - id: this.attr(null), - str: this.string('default') - } - } - } - - expect(new User({}).str).toBe('default') - expect(new User({ str: 'value' }).str).toBe('value') - expect(new User({ str: 1 }).str).toBe('1') - expect(new User({ str: true }).str).toBe('true') - expect(new User({ str: null }).str).toBe('null') - }) - - it('can mutate the value by specifying mutator at attribute', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Str('default', { mutator: (value: any) => `${value} mutated` }) - str!: string - - static fields() { - return { - id: this.attr(null), - str: this.string('default', (value: any) => `${value} mutated`) - } - } - } - - expect(new User({}).str).toBe('default mutated') - expect(new User({ str: 'value' }).str).toBe('value mutated') - expect(new User({ str: 1 }).str).toBe('1 mutated') - expect(new User({ str: true }).str).toBe('true mutated') - expect(new User({ str: null }).str).toBe('null mutated') - }) - - it('can mutate the value by specifying mutator at mutators', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: string | number - - // @Str('default') - str!: string - - static fields() { - return { - id: this.attr(null), - str: this.string('default') - } - } - - static mutators() { - return { - str(value: any) { - return `${value} mutated` - } - } - } - } - - expect(new User({}).str).toBe('default mutated') - expect(new User({ str: 'value' }).str).toBe('value mutated') - expect(new User({ str: 1 }).str).toBe('1 mutated') - expect(new User({ str: true }).str).toBe('true mutated') - expect(new User({ str: null }).str).toBe('null mutated') - }) -}) diff --git a/test/unit/model/Model_Relations.spec.ts b/test/unit/model/Model_Relations.spec.ts deleted file mode 100644 index 3f8718b9..00000000 --- a/test/unit/model/Model_Relations.spec.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* tslint:disable:variable-name */ -import Model from '@/model/Model' - -describe('Model – Relations', () => { - it('can resolve has one relation', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @HasOne(Profile, 'user_id') - profile!: Profile - - static fields() { - return { - id: this.attr(null), - profile: this.hasOne(Profile, 'user_id') - } - } - } - - class Profile extends Model { - static entity = 'profiles' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const user = new User({ - id: 1, - profile: { id: 3, user_id: 1 } - }) - - expect(user.id).toBe(1) - - expect(user.profile).toBeInstanceOf(Profile) - expect(user.profile.id).toBe(3) - }) - - it('can resolve belongs to relation', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @Attribute - user_id!: number - - // @BelongsTo(User, 'user_id') - author!: User - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null), - author: this.belongsTo(User, 'user_id') - } - } - } - - const post = new Post({ - id: 1, - user_id: 3, - author: { id: 3 } - }) - - expect(post.id).toBe(1) - - expect(post.author).toBeInstanceOf(User) - expect(post.author.id).toBe(3) - }) - - it('can resolve has many relation', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @HasMany(Comment, 'post_id') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.hasMany(Comment, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - const post = new Post({ - id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }) - - expect(post.id).toBe(1) - - expect(post.comments.length).toBe(2) - expect(post.comments[0]).toBeInstanceOf(Comment) - expect(post.comments[1]).toBeInstanceOf(Comment) - expect(post.comments[0].id).toBe(1) - expect(post.comments[1].id).toBe(2) - }) - - it('can resolve has many by relation', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @HasManyBy(Comment, 'comments') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.hasManyBy(Comment, 'comments') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - const post = new Post({ - id: 1, - comments: [ - { id: 1, post_id: 1 }, - { id: 2, post_id: 1 } - ] - }) - - expect(post).toBeInstanceOf(Post) - expect(post.comments[0]).toBeInstanceOf(Comment) - expect(post.comments[1]).toBeInstanceOf(Comment) - expect(post.comments[0].id).toBe(1) - expect(post.comments[1].id).toBe(2) - }) - - it('can resolve empty has many relation', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @HasMany(Comment, 'post_id') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.hasMany(Comment, 'post_id') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - // @Attribute - post_id!: number - - static fields() { - return { - id: this.attr(null), - post_id: this.attr(null) - } - } - } - - const post = new Post({ - id: 1, - comments: [] - }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.comments).not.toBeNull() - expect(post.comments).toEqual([]) - expect(post.comments.length).toBe(0) - }) - - it('can resolve empty has many by relation', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @HasManyBy(Comment, 'comments') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.hasManyBy(Comment, 'comments') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - const post = new Post({ - id: 1, - comments: [] - }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.comments).not.toBeNull() - expect(post.comments).toEqual([]) - expect(post.comments.length).toBe(0) - }) -}) diff --git a/test/unit/model/Model_Relations_BelongsToMany.spec.ts b/test/unit/model/Model_Relations_BelongsToMany.spec.ts deleted file mode 100644 index 722b0264..00000000 --- a/test/unit/model/Model_Relations_BelongsToMany.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Model from '@/model/Model' - -describe('Model – Relations – BelongsToMany', () => { - it('can resolve belongs to many relation', () => { - class User extends Model { - static entity = 'users' - - // @Attribute - id!: number - - // @BelongsTo(Role, RoleUser, 'user_id', 'role_id') - roles!: Role[] - - static fields() { - return { - id: this.attr(null), - roles: this.belongsToMany(Role, RoleUser, 'user_id', 'role_id') - } - } - } - - class Role extends Model { - static entity = 'roles' - - // @Attribute - id!: number - - // @BelongsToMany(User, RoleUser, 'role_id', 'user_id') - users!: User[] - - static fields() { - return { - id: this.attr(null), - users: this.belongsToMany(User, RoleUser, 'role_id', 'user_id') - } - } - } - - class RoleUser extends Model { - static entity = 'roleUser' - - static fields() { - return { - id: this.attr(null), - role_id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const user = new User({ - id: 1, - roles: [{ id: 1 }, { id: 2 }] - }) - - expect(user.id).toBe(1) - expect(user.roles[0]).toBeInstanceOf(Role) - expect(user.roles[0].id).toBe(1) - expect(user.roles[1].id).toBe(2) - }) -}) diff --git a/test/unit/model/Model_Relations_MorphMany.spec.ts b/test/unit/model/Model_Relations_MorphMany.spec.ts deleted file mode 100644 index 0cfb2ae4..00000000 --- a/test/unit/model/Model_Relations_MorphMany.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* tslint:disable:variable-name */ -import Model from '@/model/Model' - -describe('Model – Relations – MorphMany', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: string - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: string - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - it('can resolve morph many relation', () => { - const post = new Post({ - id: 1, - comments: [ - { id: 1, body: 'comment1' }, - { id: 2, body: 'comment2' } - ] - }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.comments[0]).toBeInstanceOf(Comment) - expect(post.comments[0].body).toBe('comment1') - expect(post.comments[1]).toBeInstanceOf(Comment) - expect(post.comments[1].body).toBe('comment2') - }) - - it('can resolve empty morph many relation', () => { - const post = new Post({ id: 1 }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.comments).toEqual([]) - }) -}) diff --git a/test/unit/model/Model_Relations_MorphOne.spec.ts b/test/unit/model/Model_Relations_MorphOne.spec.ts deleted file mode 100644 index 05c0ab93..00000000 --- a/test/unit/model/Model_Relations_MorphOne.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* tslint:disable:variable-name */ -import Model from '@/model/Model' - -describe('Model – Relations – Morph One', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphOne(Comment, 'commentable_id', 'commentable_type') - comment!: Comment - - static fields() { - return { - id: this.attr(null), - comment: this.morphOne(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: string - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: number - - // @Attribute - commentable_type!: string - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null) - } - } - } - - it('can resolve morph many relation', () => { - const post = new Post({ - id: 1, - comment: { id: 1, body: 'comment' } - }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.comment).toBeInstanceOf(Comment) - expect(post.comment.body).toBe('comment') - }) - - it('can resolve empty morph many relation', () => { - const post = new Post({ id: 1 }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.comment).toEqual(null) - }) -}) diff --git a/test/unit/model/Model_Relations_MorphTo.spec.ts b/test/unit/model/Model_Relations_MorphTo.spec.ts deleted file mode 100644 index 2955d967..00000000 --- a/test/unit/model/Model_Relations_MorphTo.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* tslint:disable:variable-name */ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Model – Relations – Morph To', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphMany(Comment, 'commentable_id', 'commentable_type') - comments!: Comment[] - - static fields() { - return { - id: this.attr(null), - comments: this.morphMany(Comment, 'commentable_id', 'commentable_type') - } - } - } - - class Comment extends Model { - static entity = 'comments' - - // @Attribute - id!: string - - // @Attribute('') - body!: string - - // @Attribute - commentable_id!: string | number - - // @Attribute - commentable_type!: string - - // @MorphTo('commentable_id', 'commentable_type') - commentable!: Post | Video - - static fields() { - return { - id: this.attr(null), - body: this.attr(''), - commentable_id: this.attr(null), - commentable_type: this.attr(null), - commentable: this.morphTo('commentable_id', 'commentable_type') - } - } - } - - beforeEach(() => { - createStore([{ model: Post }, { model: Video }, { model: Comment }]) - }) - - it('can resolve morph to relation', () => { - const comment = new Comment({ - id: 1, - body: 'comment1', - commentable_id: 1, - commentable_type: 'posts', - commentable: { id: 1 } - }) - - expect(comment).toBeInstanceOf(Comment) - expect(comment.id).toBe(1) - expect(comment.commentable).toBeInstanceOf(Post) - }) - - it('can resolve empty morph many relation', () => { - const comment = new Comment({ - id: 1, - body: 'comment1', - commentable_id: null, - commentable_type: null, - commentable: null - }) - - expect(comment).toBeInstanceOf(Comment) - expect(comment.id).toBe(1) - expect(comment.commentable).toEqual(null) - }) - - it('resolves unknown model to null', () => { - const comment = new Comment({ - id: 1, - body: 'comment1', - commentable_id: 1, - commentable_type: 'Unknown', - commentable: { - id: 1 - } - }) - - expect(comment).toBeInstanceOf(Comment) - expect(comment.id).toBe(1) - expect(comment.commentable).toEqual(null) - }) -}) diff --git a/test/unit/model/Model_Relations_MorphToMany.spec.ts b/test/unit/model/Model_Relations_MorphToMany.spec.ts deleted file mode 100644 index 14cc03f7..00000000 --- a/test/unit/model/Model_Relations_MorphToMany.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Model – Relations – Morph To Many', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - // @MorphToMany(Tag, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - tags!: Tag[] - - static fields() { - return { - id: this.attr(null), - tags: this.morphToMany( - Tag, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - it('can resolve morph to many relation', () => { - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - const post = new Post({ - id: 1, - tags: [ - { id: 1, name: 'news' }, - { id: 2, name: 'cast' } - ] - }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.tags[0]).toBeInstanceOf(Tag) - expect(post.tags[0].name).toBe('news') - expect(post.tags[1]).toBeInstanceOf(Tag) - expect(post.tags[1].name).toBe('cast') - }) - - it('can resolve empty morph to many relation', () => { - const post = new Post({ id: 1 }) - - expect(post).toBeInstanceOf(Post) - expect(post.id).toBe(1) - expect(post.tags).toEqual([]) - }) -}) diff --git a/test/unit/model/Model_Relations_MorphedByMany.spec.ts b/test/unit/model/Model_Relations_MorphedByMany.spec.ts deleted file mode 100644 index b7e462c2..00000000 --- a/test/unit/model/Model_Relations_MorphedByMany.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Model – Relations – Morphed By Many', () => { - class Post extends Model { - static entity = 'posts' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Video extends Model { - static entity = 'videos' - - // @Attribute - id!: number - - static fields() { - return { - id: this.attr(null) - } - } - } - - class Tag extends Model { - static entity = 'tags' - - // @Attribute - id!: number - - // @Attribute('') - name!: string - - // @MorphedByMany(Post, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - posts!: Post[] - - // @MorphedByMany(Video, Taggable, 'tag_id', 'taggable_id', 'taggable_type') - videos!: Video[] - - static fields() { - return { - id: this.attr(null), - name: this.attr(''), - posts: this.morphedByMany( - Post, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ), - videos: this.morphedByMany( - Video, - Taggable, - 'tag_id', - 'taggable_id', - 'taggable_type' - ) - } - } - } - - class Taggable extends Model { - static entity = 'taggables' - - static fields() { - return { - id: this.attr(null), - tag_id: this.attr(null), - taggable_id: this.attr(null), - taggable_type: this.attr(null) - } - } - } - - it('can resolve morphed by many relation', () => { - createStore([ - { model: Post }, - { model: Video }, - { model: Tag }, - { model: Taggable } - ]) - - const tag = new Tag({ - id: 1, - name: 'news', - posts: [{ id: 1 }, { id: 2 }], - videos: [{ id: 3 }] - }) - - expect(tag).toBeInstanceOf(Tag) - expect(tag.id).toBe(1) - expect(tag.posts.length).toBe(2) - expect(tag.posts[0]).toBeInstanceOf(Post) - expect(tag.videos.length).toBe(1) - expect(tag.videos[0]).toBeInstanceOf(Video) - }) - - it('can resolve empty morphed by many relation', () => { - const tag = new Tag({ id: 1 }) - - expect(tag).toBeInstanceOf(Tag) - expect(tag.id).toBe(1) - expect(tag.posts).toEqual([]) - expect(tag.videos).toEqual([]) - }) -}) diff --git a/test/unit/model/Model_Serialization.spec.ts b/test/unit/model/Model_Serialization.spec.ts deleted file mode 100644 index 7e09f197..00000000 --- a/test/unit/model/Model_Serialization.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -import Model from '@/model/Model' - -describe('Unit – Model - Serialization', () => { - it('can serialize own fields into json', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - const user = new User({ id: 1, name: 'John Doe' }) - - const json = user.$toJson() - - expect(json).not.toBeInstanceOf(User) - expect(json).toEqual({ id: 1, name: 'John Doe' }) - }) - - it('can serialize nested fields into json', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id'), - posts: this.hasMany(Post, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - class Post extends Model { - static entity = 'posts' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const user = new User({ - id: 1, - phone: { id: 2, user_id: 1 }, - posts: [ - { id: 3, user_id: 1 }, - { id: 4, user_id: 1 } - ] - }) - - const json = user.$toJson() - - const expected = { - id: 1, - phone: { id: 2, user_id: 1 }, - posts: [ - { id: 3, user_id: 1 }, - { id: 4, user_id: 1 } - ] - } - - expect(json).not.toBeInstanceOf(User) - expect(json.phone).not.toBeInstanceOf(Phone) - expect(json.posts[0]).not.toBeInstanceOf(Post) - expect(json.posts[1]).not.toBeInstanceOf(Post) - expect(json).toEqual(expected) - }) - - it('can serialize empty relation into json', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - phone: this.hasOne(Phone, 'user_id') - } - } - } - - class Phone extends Model { - static entity = 'phones' - - static fields() { - return { - id: this.attr(null), - user_id: this.attr(null) - } - } - } - - const user = new User({ id: 1 }) - - const json = user.$toJson() - - const expected = { id: 1, phone: null } - - expect(json).not.toBeInstanceOf(User) - expect(json).toEqual(expected) - }) - - it('can serialize the array field', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - array: this.attr([]) - } - } - } - - const user = new User({ id: 1, array: [1, 2] }) - - const json = user.$toJson() - - expect(json).not.toBeInstanceOf(User) - expect(json).toEqual({ id: 1, array: [1, 2] }) - }) -}) diff --git a/test/unit/model/Model_Store.spec.ts b/test/unit/model/Model_Store.spec.ts deleted file mode 100644 index fa18400f..00000000 --- a/test/unit/model/Model_Store.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' - -describe('Model – Store', () => { - it('can access the store instance as static method', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - static find() { - return this.store().getters[this.namespace('find')](1) - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const user = User.find() - - expect(user.id).toBe(1) - expect(user.name).toBe('John Doe') - }) - - it('can access the store instance as instance method', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - create() { - return this.$store().dispatch(this.$namespace('create'), { - data: { id: 1, name: 'John Doe' } - }) - } - } - - const store = createStore([{ model: User }]) - - const u = new User() - await u.create() - - const user = store.getters['entities/users/find'](1) - - expect(user.id).toBe(1) - expect(user.name).toBe('John Doe') - }) - - it('can dispatch an action as static method', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - static create() { - return this.dispatch('create', { - data: { id: 1, name: 'John Doe' } - }) - } - } - - const store = createStore([{ model: User }]) - - await User.create() - - const user = store.getters['entities/users/find'](1) - - expect(user.id).toBe(1) - expect(user.name).toBe('John Doe') - }) - - it('can dispatch an action as instance method', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - create() { - return this.$dispatch('create', { - data: { id: 1, name: 'John Doe' } - }) - } - } - - const store = createStore([{ model: User }]) - - const u = new User() - await u.create() - - const user = store.getters['entities/users/find'](1) - - expect(user.id).toBe(1) - expect(user.name).toBe('John Doe') - }) - - it('can call getters as static method', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - static find() { - return this.getters('find')(1) - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const user = User.find() - - expect(user.id).toBe(1) - expect(user.name).toBe('John Doe') - }) - - it('can call getters as instance method', async () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - - find() { - return this.$getters('find')(1) - } - } - - const store = createStore([{ model: User }]) - - await store.dispatch('entities/users/create', { - data: { id: 1, name: 'John Doe' } - }) - - const user = new User().find() - - expect(user.id).toBe(1) - expect(user.name).toBe('John Doe') - }) -}) diff --git a/test/unit/model/Model_Utilities.spec.ts b/test/unit/model/Model_Utilities.spec.ts deleted file mode 100644 index fc1eeab2..00000000 --- a/test/unit/model/Model_Utilities.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import Model from '@/model/Model' - -describe('Unit – Model - Utilities', () => { - it('can check if the key is primary key', () => { - class User extends Model { - static entity = 'users' - - static primaryKey = 'id' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - expect(User.isPrimaryKey('id')).toBe(true) - expect(User.isPrimaryKey('name')).toBe(false) - }) - - it('can check if the key is primary key is included in composite primary key', () => { - class Subscription extends Model { - static entity = 'users' - - static primaryKey = ['user_id', 'video_id'] - - static fields() { - return { - user_id: this.attr(null), - video_id: this.attr(null), - created_at: this.attr(null) - } - } - } - - expect(Subscription.isPrimaryKey('user_id')).toBe(true) - expect(Subscription.isPrimaryKey('video_id')).toBe(true) - expect(Subscription.isPrimaryKey('created_at')).toBe(false) - }) - - it('can check if the primary key is a composite primary key', () => { - class Subscription extends Model { - static entity = 'users' - - static primaryKey = ['user_id', 'video_id'] - - static fields() { - return { - user_id: this.attr(null), - video_id: this.attr(null), - created_at: this.attr(null) - } - } - } - - expect(Subscription.isCompositePrimaryKey()).toBe(true) - }) - - it('can get primary key value out of record', () => { - class User extends Model { - static entity = 'users' - - static fields() { - return { - id: this.attr(null), - name: this.attr('') - } - } - } - - expect(User.getIdFromRecord({ id: 1, name: 'John' })).toBe(1) - expect(User.getIdFromRecord({ name: 'John' })).toBe(null) - }) - - it('can get composite primary key value out of record as an array ', () => { - class Subscription extends Model { - static entity = 'users' - - static primaryKey = ['user_id', 'video_id'] - - static fields() { - return { - user_id: this.attr(null), - video_id: this.attr(null), - created_at: this.attr(null) - } - } - } - - expect(Subscription.getIdFromRecord({ user_id: 1, video_id: 2 })).toEqual([ - 1, - 2 - ]) - expect( - Subscription.getIdFromRecord({ created_at: '1985-10-10 12:00:00' }) - ).toBe(null) - }) -}) diff --git a/test/unit/query/Query.spec.ts b/test/unit/query/Query.spec.ts deleted file mode 100644 index 9f61c7f8..00000000 --- a/test/unit/query/Query.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createStore } from 'test/support/Helpers' -import Model from '@/model/Model' -import Query from '@/query/Query' - -describe('Unit – Query', () => { - class User extends Model { - static entity = 'users' - } - - class Post extends Model { - static entity = 'posts' - } - - it('can retrieve all models from the database instance', () => { - const store = createStore([{ model: User }, { model: Post }]) - - const models = new Query(store, 'users').getModels() - - expect(models.users).toBe(User) - expect(models.posts).toBe(Post) - }) - - it('can retrieve a specific model from the database instance', () => { - const store = createStore([{ model: User }, { model: Post }]) - - const model = new Query(store, 'users').getModel('posts') - - expect(model).toBe(Post) - }) - - it('retrieves the model corresponds to the entity of Query if the name is not specified', () => { - const store = createStore([{ model: User }, { model: Post }]) - - const model = new Query(store, 'users').getModel() - - expect(model).toBe(User) - }) -}) diff --git a/test/unit/support/Utils.spec.ts b/test/unit/support/Utils.spec.ts deleted file mode 100644 index ee9121ee..00000000 --- a/test/unit/support/Utils.spec.ts +++ /dev/null @@ -1,138 +0,0 @@ -import Utils from '@/support/Utils' - -describe('Unit - Utils', () => { - describe('#orderBy', () => { - it('can order collection by given key in asc order', () => { - const collection = [{ id: 2 }, { id: 3 }, { id: 10 }, { id: 1 }] - - const expected = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 10 }] - - expect(Utils.orderBy(collection, ['id'], ['asc'])).toEqual(expected) - }) - - it('can order collection by given key in desc order', () => { - const collection = [{ id: 2 }, { id: 3 }, { id: 10 }, { id: 1 }] - - const expected = [{ id: 10 }, { id: 3 }, { id: 2 }, { id: 1 }] - - expect(Utils.orderBy(collection, ['id'], ['desc'])).toEqual(expected) - }) - - it('can order collection of mixed value types', () => { - const collection = [ - { id: 2 }, - { id: 'id3' }, - { name: 'John Doe' }, - { name: 'Andy Newman' }, - { id: null }, - { id: 'id1' }, - { id: 1 } - ] - - const expected = [ - { id: 1 }, - { id: 2 }, - { id: 'id1' }, - { id: 'id3' }, - { id: null }, - { name: 'John Doe' }, - { name: 'Andy Newman' } - ] - - expect(Utils.orderBy(collection, ['id'], ['asc'])).toEqual(expected) - }) - - it('can order collection with multiple keys', () => { - const collection = [ - { id: 1, name: 'John Doe' }, - { id: 3, name: 'Peter Ericsson' }, - { id: 2, name: 'Andrew Black' }, - { id: 3, name: 'George Mac' }, - { id: 1, name: 'Bob Green' }, - { id: 2, name: 'Chris Brian' } - ] - - const expected = [ - { id: 1, name: 'Bob Green' }, - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Andrew Black' }, - { id: 2, name: 'Chris Brian' }, - { id: 3, name: 'George Mac' }, - { id: 3, name: 'Peter Ericsson' } - ] - - expect(Utils.orderBy(collection, ['id', 'name'], ['asc', 'asc'])).toEqual( - expected - ) - }) - - it('can order collection with multiple keys with mixed key types', () => { - const collection = [ - { id: 1, name: 'John Doe' }, - { name: 'Peter Ericsson' }, - { id: 2, name: 'Andrew Black' }, - { name: 'George Mac' }, - { id: 1 }, - { id: 2, name: 'Chris Brian' } - ] - - const expected = [ - { id: 1 }, - { id: 1, name: 'John Doe' }, - { id: 2, name: 'Chris Brian' }, - { id: 2, name: 'Andrew Black' }, - { name: 'Peter Ericsson' }, - { name: 'George Mac' } - ] - - expect( - Utils.orderBy(collection, ['id', 'name'], ['asc', 'desc']) - ).toEqual(expected) - }) - - it('can order collection with function', () => { - const collection = [{ id: 2 }, { id: 3 }, { id: 1 }] - - const expected = [{ id: 1 }, { id: 2 }, { id: 3 }] - - expect(Utils.orderBy(collection, [(v) => v.id], ['asc'])).toEqual( - expected - ) - }) - }) - - describe('#cloneDeep', () => { - it('can create a deep clone of an object', () => { - const data = { - id: 1, - nested: [ - { - id: 1, - deep: [ - { - id: 1, - deeper: { - id: 2 - } - }, - { - id: 2, - deeper: { - id: 3 - } - } - ] - } - ] - } - - const clone = Utils.cloneDeep(data) - - expect(clone).toStrictEqual(data) - - delete data.nested[0].deep[0].deeper.id - - expect(clone).not.toStrictEqual(data) - }) - }) -}) From fc943046358a362a5da318910935f50ea3a26e16 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 01:57:04 +0900 Subject: [PATCH 17/56] chore: refresh jest ignore settings --- jest.config.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jest.config.js b/jest.config.js index dbba5e5d..70eb10d1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,10 +14,6 @@ module.exports = { coverageReporters: ['json', 'lcov', 'text-summary', 'clover'], collectCoverageFrom: [ 'src/**/*.ts', - '!src/attributes/types/Increment.ts', - '!src/polyfills/index.ts', - '!src/support/Utils.ts', - '!src/index.cjs.ts', - '!src/index.ts' + '!src/index.cjs.ts' ] } From 307e4dc00a4534244d69328a21cf3729886cfd69 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 01:57:37 +0900 Subject: [PATCH 18/56] feat: add installation feature --- src/data/Data.ts | 7 +++ src/database/Database.ts | 119 ++++++++++++++++++++++++++++++++++++++ src/index.cjs.ts | 10 +++- src/index.ts | 12 +++- src/model/Model.ts | 6 ++ src/modules/Module.ts | 7 +++ src/modules/RootModule.ts | 8 +++ src/modules/RootState.ts | 7 +++ src/modules/State.ts | 7 +++ src/store/Store.ts | 62 ++++++++++++++++++++ src/types/index.ts | 1 + src/types/vuex.d.ts | 10 ++++ test/unit/VuexORM.spec.ts | 68 ++++++++++++++++++++++ 13 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 src/data/Data.ts create mode 100644 src/database/Database.ts create mode 100644 src/model/Model.ts create mode 100644 src/modules/Module.ts create mode 100644 src/modules/RootModule.ts create mode 100644 src/modules/RootState.ts create mode 100644 src/modules/State.ts create mode 100644 src/store/Store.ts create mode 100644 src/types/index.ts create mode 100644 src/types/vuex.d.ts create mode 100644 test/unit/VuexORM.spec.ts diff --git a/src/data/Data.ts b/src/data/Data.ts new file mode 100644 index 00000000..555077af --- /dev/null +++ b/src/data/Data.ts @@ -0,0 +1,7 @@ +export interface Record { + [key: string]: any +} + +export interface Records { + [id: string]: Record +} diff --git a/src/database/Database.ts b/src/database/Database.ts new file mode 100644 index 00000000..4984f036 --- /dev/null +++ b/src/database/Database.ts @@ -0,0 +1,119 @@ +import { Store, Module as VuexModule } from 'vuex' +import { ConstructorOf } from '../types' +import Model from '../model/Model' +import RootModule from '../modules/RootModule' +import Module from '../modules/Module' +import State from '../modules/State' + +interface Models { + [name: string]: ConstructorOf +} + +export default class Database { + /** + * The store instance. + */ + store!: Store + + /** + * The name of Vuex Module namespace. Vuex ORM will create Vuex Modules from + * the registered models and modules and define them under this namespace. + */ + connection!: string + + /** + * The list of registered models. + */ + models: Models = {} + + /** + * Whether the database has already been installed to Vuex or not. + * The model registration steps depend on its value. + */ + started: boolean = false + + /** + * Register the given model. + */ + register(model: ConstructorOf): void { + const m = new model() + + this.models[m.entity] = model + } + + /** + * Connect the store. + */ + connectStore(store: Store): this { + this.store = store + + return this + } + + /** + * Set the connection. + */ + setConnection(connection: string): this { + this.connection = connection + + return this + } + + /** + * Initialize the database before a user can start using it. + */ + start(): void { + this.registerModules() + + this.started = true + } + + /** + * Generate modules and register them to the store. + */ + private registerModules(): void { + this.store.registerModule(this.connection, this.createModule()) + } + + /** + * Create modules from the registered models and modules. + */ + private createModule(): VuexModule { + const module = this.createRootModule() + + for (const name in this.models) { + module.modules[name] = this.createSubModule() + } + + return module + } + + /** + * Create root module. + */ + private createRootModule(): RootModule { + return { + namespaced: true, + modules: {} + } + } + + /** + * Create sub module. + */ + private createSubModule(): Module { + return { + namespaced: true, + state: this.createSubState() + } + } + + /** + * Create sub state. + */ + private createSubState(): State { + return { + data: {} + } + } +} diff --git a/src/index.cjs.ts b/src/index.cjs.ts index c15f26f4..91f17404 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1 +1,9 @@ -// ... +import { install } from './store/Store' +import Database from './database/Database' +import Model from './model/Model' + +export default { + install, + Database, + Model +} diff --git a/src/index.ts b/src/index.ts index c15f26f4..94f28ed2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,11 @@ -// ... +import { install, Options } from './store/Store' +import Database from './database/Database' +import Model from './model/Model' + +export { install, Options, Database, Model } + +export default { + install, + Database, + Model +} diff --git a/src/model/Model.ts b/src/model/Model.ts new file mode 100644 index 00000000..4cc7e283 --- /dev/null +++ b/src/model/Model.ts @@ -0,0 +1,6 @@ +export default abstract class Model { + /** + * The name of the model. + */ + entity!: string +} diff --git a/src/modules/Module.ts b/src/modules/Module.ts new file mode 100644 index 00000000..5c4153aa --- /dev/null +++ b/src/modules/Module.ts @@ -0,0 +1,7 @@ +import { Module as BaseModule } from 'vuex' +import State from './State' + +export default interface Module extends BaseModule { + namespaced: true + state: S +} diff --git a/src/modules/RootModule.ts b/src/modules/RootModule.ts new file mode 100644 index 00000000..abf17a84 --- /dev/null +++ b/src/modules/RootModule.ts @@ -0,0 +1,8 @@ +import { Module as BaseModule, ModuleTree } from 'vuex' +import RootState from './RootState' + +export default interface RootModule + extends BaseModule { + namespaced: true + modules: ModuleTree +} diff --git a/src/modules/RootState.ts b/src/modules/RootState.ts new file mode 100644 index 00000000..891618e8 --- /dev/null +++ b/src/modules/RootState.ts @@ -0,0 +1,7 @@ +import State from './State' + +interface RootState { + [entity: string]: State +} + +export default RootState diff --git a/src/modules/State.ts b/src/modules/State.ts new file mode 100644 index 00000000..e0855870 --- /dev/null +++ b/src/modules/State.ts @@ -0,0 +1,7 @@ +import { Records } from '../data/Data' + +interface State { + data: Records +} + +export default State diff --git a/src/store/Store.ts b/src/store/Store.ts new file mode 100644 index 00000000..d7cc7896 --- /dev/null +++ b/src/store/Store.ts @@ -0,0 +1,62 @@ +import { Store, Plugin } from 'vuex' +import Database from '../database/Database' + +export interface Options { + namespace?: string +} + +type FilledOptions = Required + +/** + * Install Vuex ORM to the store. + */ +export function install( + database: Database, + options?: Options +): Plugin { + return (store) => { + mixin(store, database, createOptions(options)) + } +} + +/** + * Create options by merging the given user-provided options. + */ +function createOptions(options: Options = {}): FilledOptions { + return { + namespace: options.namespace ?? 'entities' + } +} + +/** + * Mixin Vuex ORM feature to the store. + */ +function mixin( + store: Store, + database: Database, + options: FilledOptions +): void { + connectDatabase(store, database, options) + + startDatabase(store) +} + +/** + * Connect the database to the store. + */ +function connectDatabase( + store: Store, + database: Database, + options: FilledOptions +): void { + database.connectStore(store).setConnection(options.namespace) + + store.$database = database +} + +/** + * Start the database. + */ +function startDatabase(store: Store): void { + store.$database.start() +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..c5ddf026 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export type ConstructorOf = { new (...args: any[]): C } diff --git a/src/types/vuex.d.ts b/src/types/vuex.d.ts new file mode 100644 index 00000000..06b8086d --- /dev/null +++ b/src/types/vuex.d.ts @@ -0,0 +1,10 @@ +import Database from '../database/Database' + +declare module 'vuex' { + interface Store { + /** + * The database instance. + */ + $database: Database + } +} diff --git a/test/unit/VuexORM.spec.ts b/test/unit/VuexORM.spec.ts new file mode 100644 index 00000000..be67e61e --- /dev/null +++ b/test/unit/VuexORM.spec.ts @@ -0,0 +1,68 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import VuexORM, { Database, Model } from '@/index' + +describe('unit/VuexORM', () => { + Vue.use(Vuex) + + it('installs Vuex ORM to the store', () => { + class User extends Model { + entity = 'users' + } + + class Post extends Model { + entity = 'posts' + } + + const database = new Database() + + database.register(User) + database.register(Post) + + const store = new Vuex.Store({ + plugins: [VuexORM.install(database)] + }) + + const expected = { + entities: { + users: { + data: {} + }, + posts: { + data: {} + } + } + } + + expect(store.state).toEqual(expected) + expect(store.$database.started).toBe(true) + }) + + it('can customize the namespace', () => { + class User extends Model { + entity = 'users' + } + + const database = new Database() + + database.register(User) + + const store = new Vuex.Store({ + plugins: [ + VuexORM.install(database, { + namespace: 'database' + }) + ] + }) + + const expected = { + database: { + users: { + data: {} + } + } + } + + expect(store.state).toEqual(expected) + }) +}) From e087c4a7325604baa370b3d2a8732893c5973bbf Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 02:00:06 +0900 Subject: [PATCH 19/56] style: fix linter issue --- src/store/Store.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/store/Store.ts b/src/store/Store.ts index d7cc7896..4679a7d5 100644 --- a/src/store/Store.ts +++ b/src/store/Store.ts @@ -10,10 +10,7 @@ type FilledOptions = Required /** * Install Vuex ORM to the store. */ -export function install( - database: Database, - options?: Options -): Plugin { +export function install(database: Database, options?: Options): Plugin { return (store) => { mixin(store, database, createOptions(options)) } From 71a8a65c48da5cf396870893e2dd691315aee08b Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 02:05:49 +0900 Subject: [PATCH 20/56] feat: export data and modules --- src/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 94f28ed2..69ba40fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,23 @@ +export * from './data/Data' + import { install, Options } from './store/Store' import Database from './database/Database' import Model from './model/Model' +import RootModule from './modules/RootModule' +import RootState from './modules/RootState' +import Module from './modules/Module' +import State from './modules/State' -export { install, Options, Database, Model } +export { + install, + Options, + Database, + Model, + RootModule, + RootState, + Module, + State +} export default { install, From de5a5b6fa83556139f7bfa982858f015ac2dbabe Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 02:06:28 +0900 Subject: [PATCH 21/56] style: directly export module interfaces --- src/modules/RootState.ts | 4 +--- src/modules/State.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/RootState.ts b/src/modules/RootState.ts index 891618e8..f5bf666a 100644 --- a/src/modules/RootState.ts +++ b/src/modules/RootState.ts @@ -1,7 +1,5 @@ import State from './State' -interface RootState { +export default interface RootState { [entity: string]: State } - -export default RootState diff --git a/src/modules/State.ts b/src/modules/State.ts index e0855870..c0a9972a 100644 --- a/src/modules/State.ts +++ b/src/modules/State.ts @@ -1,7 +1,5 @@ import { Records } from '../data/Data' -interface State { +export default interface State { data: Records } - -export default State From 1e41d741c681c4b405a17078b20ad85b12a77235 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 02:31:51 +0900 Subject: [PATCH 22/56] refactor(database): rename `connectStore` to `setStore` --- src/database/Database.ts | 2 +- src/store/Store.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/Database.ts b/src/database/Database.ts index 4984f036..fa5f321c 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -44,7 +44,7 @@ export default class Database { /** * Connect the store. */ - connectStore(store: Store): this { + setStore(store: Store): this { this.store = store return this diff --git a/src/store/Store.ts b/src/store/Store.ts index 4679a7d5..3d459780 100644 --- a/src/store/Store.ts +++ b/src/store/Store.ts @@ -46,7 +46,7 @@ function connectDatabase( database: Database, options: FilledOptions ): void { - database.connectStore(store).setConnection(options.namespace) + database.setStore(store).setConnection(options.namespace) store.$database = database } From 46078880b62ec085bd7d79a7cf7839e74e03763d Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 02:35:22 +0900 Subject: [PATCH 23/56] chore(database): update comment --- src/database/Database.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/Database.ts b/src/database/Database.ts index fa5f321c..a96e06a1 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -42,7 +42,7 @@ export default class Database { } /** - * Connect the store. + * Set the store. */ setStore(store: Store): this { this.store = store From dcde28f6687dcb83e639c6888448a172c033251d Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 15:22:47 +0900 Subject: [PATCH 24/56] feat: add insert feature --- src/connection/Connection.ts | 51 +++++++ src/data/Data.ts | 14 ++ src/database/Database.ts | 68 +++++++-- src/index.ts | 5 +- src/interpretation/Interpretation.ts | 47 +++++++ src/model/Constructor.ts | 8 ++ src/model/Model.ts | 105 +++++++++++++- src/model/attributes/Attribute.ts | 20 +++ src/model/attributes/Attributes.ts | 5 + src/model/attributes/types/Attr.ts | 11 ++ src/model/attributes/types/Type.ts | 6 + .../decorators/contracts/PropertyDecorator.ts | 5 + src/model/decorators/types/Attr.ts | 13 ++ src/modules/Mutations.ts | 13 ++ src/query/Query.ts | 57 ++++++++ src/repository/Repository.ts | 130 ++++++++++++++++++ src/schema/Schema.ts | 48 +++++++ src/store/Store.ts | 16 +++ src/support/Utils.ts | 20 +++ src/types/index.ts | 1 - src/types/vuex.d.ts | 8 ++ test/Helpers.ts | 39 +++++- test/feature/query/inserts_insert.spec.ts | 44 ++++++ test/unit/VuexORM.spec.ts | 6 +- tsconfig.json | 1 + 25 files changed, 725 insertions(+), 16 deletions(-) create mode 100644 src/connection/Connection.ts create mode 100644 src/interpretation/Interpretation.ts create mode 100644 src/model/Constructor.ts create mode 100644 src/model/attributes/Attribute.ts create mode 100644 src/model/attributes/Attributes.ts create mode 100644 src/model/attributes/types/Attr.ts create mode 100644 src/model/attributes/types/Type.ts create mode 100644 src/model/decorators/contracts/PropertyDecorator.ts create mode 100644 src/model/decorators/types/Attr.ts create mode 100644 src/modules/Mutations.ts create mode 100644 src/query/Query.ts create mode 100644 src/repository/Repository.ts create mode 100644 src/schema/Schema.ts create mode 100644 src/support/Utils.ts delete mode 100644 src/types/index.ts create mode 100644 test/feature/query/inserts_insert.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts new file mode 100644 index 00000000..59c38d2d --- /dev/null +++ b/src/connection/Connection.ts @@ -0,0 +1,51 @@ +import { Store } from 'vuex' +import { Record, Records } from '../data/Data' +import Model from '../model/Model' +import Constructor from '../model/Constructor' + +export default class Connection { + /** + * The store instance. + */ + store: Store + + /** + * The model object. + */ + model: Constructor + + /** + * Create a new connection instance. + */ + constructor(store: Store, model: Constructor) { + this.store = store + this.model = model + } + + /** + * Commit the store mutation. + */ + private commit(name: string, payload: any): void { + const namespace = this.store.$database.connection + const entity = this.model.entity + + this.store.commit(`${namespace}/${entity}/${name}`, payload) + } + + /** + * Commit `insert` change to the store. + */ + insert(records: Record[]): void { + this.commit('insert', this.mapRecords(records)) + } + + /** + * Convert the given array of records into records. + */ + private mapRecords(records: Record[]): Records { + return records.reduce((carry, record) => { + carry[this.model.getIndexId(record)] = record + return carry + }, {}) + } +} diff --git a/src/data/Data.ts b/src/data/Data.ts index 555077af..d7ead5d9 100644 --- a/src/data/Data.ts +++ b/src/data/Data.ts @@ -1,3 +1,5 @@ +import Model from '../model/Model' + export interface Record { [key: string]: any } @@ -5,3 +7,15 @@ export interface Record { export interface Records { [id: string]: Record } + +export interface NormalizedData { + [entity: string]: Records +} + +export type Item = M | null + +export type Collection = M[] + +export interface Collections { + [name: string]: Collection +} diff --git a/src/database/Database.ts b/src/database/Database.ts index a96e06a1..719f7837 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -1,12 +1,19 @@ -import { Store, Module as VuexModule } from 'vuex' -import { ConstructorOf } from '../types' +import { Store, Module as VuexModule, MutationTree } from 'vuex' +import { schema as Normalizr } from 'normalizr' +import Schema from '../schema/Schema' import Model from '../model/Model' +import Constructor from '../model/Constructor' import RootModule from '../modules/RootModule' import Module from '../modules/Module' import State from '../modules/State' +import Mutations from '../modules/Mutations' interface Models { - [name: string]: ConstructorOf + [name: string]: Constructor +} + +interface Schemas { + [name: string]: Normalizr.Entity } export default class Database { @@ -26,6 +33,11 @@ export default class Database { */ models: Models = {} + /** + * The schema definition for the registered models. + */ + schemas: Schemas = {} + /** * Whether the database has already been installed to Vuex or not. * The model registration steps depend on its value. @@ -35,10 +47,8 @@ export default class Database { /** * Register the given model. */ - register(model: ConstructorOf): void { - const m = new model() - - this.models[m.entity] = model + register(model: Constructor): void { + this.models[model.entity] = model } /** @@ -63,11 +73,45 @@ export default class Database { * Initialize the database before a user can start using it. */ start(): void { + this.createSchemas() + this.registerModules() this.started = true } + /** + * Get the model. + */ + getModel(name: string): Constructor { + return this.models[name] as Constructor + } + + /** + * Get the schema. + */ + getSchema(name: string): Normalizr.Entity { + return this.schemas[name] + } + + /** + * Create the schema definition from registered models and set it to the + * `schema` property. This schema will be used by the interpretation + * to interpret data before persisting them to the store. + */ + private createSchemas(): void { + for (const name in this.models) { + this.schemas[name] = this.createSchema(this.models[name]) + } + } + + /** + * Create schema from the given model. + */ + private createSchema(model: Constructor): Normalizr.Entity { + return new Schema(model).one() + } + /** * Generate modules and register them to the store. */ @@ -104,7 +148,8 @@ export default class Database { private createSubModule(): Module { return { namespaced: true, - state: this.createSubState() + state: this.createSubState(), + mutations: this.createSubMutations() } } @@ -116,4 +161,11 @@ export default class Database { data: {} } } + + /** + * Create sub mutations. + */ + private createSubMutations(): MutationTree { + return Mutations + } } diff --git a/src/index.ts b/src/index.ts index 69ba40fa..4ed0458c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export * from './data/Data' import { install, Options } from './store/Store' import Database from './database/Database' import Model from './model/Model' +import Attr from './model/decorators/types/Attr' import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' @@ -13,6 +14,7 @@ export { Options, Database, Model, + Attr, RootModule, RootState, Module, @@ -22,5 +24,6 @@ export { export default { install, Database, - Model + Model, + Attr } diff --git a/src/interpretation/Interpretation.ts b/src/interpretation/Interpretation.ts new file mode 100644 index 00000000..466233c2 --- /dev/null +++ b/src/interpretation/Interpretation.ts @@ -0,0 +1,47 @@ +import { normalize, schema as Normalizr } from 'normalizr' +import { Store } from 'vuex' +import { isArray, isEmpty } from '../support/Utils' +import { Record, NormalizedData } from '../data/Data' +import Model from '../model/Model' +import Constructor from '../model/Constructor' + +export default class Interpretation { + /** + * The store instance. + */ + store: Store + + /** + * The model object. + */ + model: Constructor + + /** + * Create a new interpreter instance. + */ + constructor(store: Store, model: Constructor) { + this.store = store + this.model = model + } + + /** + * Perform interpretation for the given data. + */ + process(data: Record | Record[]): NormalizedData { + if (isEmpty(data)) { + return {} + } + + const schema = this.getSchema() + + return normalize(data, isArray(data) ? [schema] : schema) + .entities as NormalizedData + } + + /** + * Get the schema from the database. + */ + private getSchema(): Normalizr.Entity { + return this.store.$database.getSchema(this.model.entity) + } +} diff --git a/src/model/Constructor.ts b/src/model/Constructor.ts new file mode 100644 index 00000000..cf438617 --- /dev/null +++ b/src/model/Constructor.ts @@ -0,0 +1,8 @@ +import { Record } from '../data/Data' +import Model from './Model' + +export default interface Constructor { + entity: string + new (...args: any[]): M + getIndexId(record: Record): string +} diff --git a/src/model/Model.ts b/src/model/Model.ts index 4cc7e283..1ab7a9cc 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,6 +1,107 @@ -export default abstract class Model { +import { Record } from '../data/Data' +import * as Attributes from './attributes/Attributes' + +interface Fields { + [key: string]: Attributes.Attribute +} + +export default class Model { /** * The name of the model. */ - entity!: string + static entity: string + + /** + * The primary key for the model. + */ + static primaryKey: string = 'id' + + /** + * The schema for the model. It contains the result of the `fields` + * method or the attributes defined by decorators. + */ + protected static schema: Fields = {} + + /** + * Create a new model instance. + */ + constructor(record: Record) { + this.$fill(record) + } + + /** + * Set the attribute. + */ + static setSchema( + this: M, + key: string, + attribute: Attributes.Attribute + ): M { + this.schema[key] = attribute + + return this + } + + /** + * Create a new attr attribute class. + */ + static attr(this: M): Attributes.Attribute { + return new Attributes.Attr(this) + } + + /** + * Get the index id from the given record. An index ID is a value that + * used as a index key for records within the store. + */ + static getIndexId(record: Record): string { + return record[this.primaryKey] + } + + /** + * Get the constructor for the model. + */ + get $self(): typeof Model { + return this.constructor as typeof Model + } + + /** + * Get model fields for the model. + */ + $fields(): Fields { + return this.$self.schema + } + + /** + * Fill the model by the given attributes. Its default values will fill any + * missing field. + */ + $fill(attributes: Record): this { + const fields = this.$fields() + + for (const key in fields) { + const attr = fields[key] + const value = attributes[key] + + this[key] = attr.make(value) + } + + return this + } + + /** + * Get the serialized model attributes. + */ + $getAttributes(): Record { + const fields = this.$fields() + + const record = {} as Record + + for (const key in fields) { + const value = this[key] + + record[key] = value + } + + return record + } } diff --git a/src/model/attributes/Attribute.ts b/src/model/attributes/Attribute.ts new file mode 100644 index 00000000..6304e882 --- /dev/null +++ b/src/model/attributes/Attribute.ts @@ -0,0 +1,20 @@ +import Model from '../Model' + +export default abstract class Attribute { + /** + * The model instance. + */ + protected model: M + + /** + * Create a new attribute instance. + */ + constructor(model: M) { + this.model = model + } + + /** + * Make the value for the attribute. + */ + abstract make(value: any): any +} diff --git a/src/model/attributes/Attributes.ts b/src/model/attributes/Attributes.ts new file mode 100644 index 00000000..0541515e --- /dev/null +++ b/src/model/attributes/Attributes.ts @@ -0,0 +1,5 @@ +import Attribute from './Attribute' +import Type from './types/Type' +import Attr from './types/Attr' + +export { Attribute, Type, Attr } diff --git a/src/model/attributes/types/Attr.ts b/src/model/attributes/types/Attr.ts new file mode 100644 index 00000000..4e0d444f --- /dev/null +++ b/src/model/attributes/types/Attr.ts @@ -0,0 +1,11 @@ +import Model from '../../Model' +import Type from './Type' + +export default class Attr extends Type { + /** + * Make the value for the attribute. + */ + make(value: any): any { + return value + } +} diff --git a/src/model/attributes/types/Type.ts b/src/model/attributes/types/Type.ts new file mode 100644 index 00000000..7c5a2546 --- /dev/null +++ b/src/model/attributes/types/Type.ts @@ -0,0 +1,6 @@ +import Model from '../../Model' +import Attribute from '../Attribute' + +export default abstract class Type extends Attribute< + M +> {} diff --git a/src/model/decorators/contracts/PropertyDecorator.ts b/src/model/decorators/contracts/PropertyDecorator.ts new file mode 100644 index 00000000..bc138ed9 --- /dev/null +++ b/src/model/decorators/contracts/PropertyDecorator.ts @@ -0,0 +1,5 @@ +import Model from '../../Model' + +type PropertyDecorator = (target: Model, propertyKey: string) => void + +export default PropertyDecorator diff --git a/src/model/decorators/types/Attr.ts b/src/model/decorators/types/Attr.ts new file mode 100644 index 00000000..954396f9 --- /dev/null +++ b/src/model/decorators/types/Attr.ts @@ -0,0 +1,13 @@ +import Model from '../../Model' +import PropertyDecorator from '../contracts/PropertyDecorator' + +/** + * Create an attr decorator. + */ +export default function Attr(): PropertyDecorator { + return (target, propertyKey) => { + const model = target.constructor as typeof Model + + model.setSchema(propertyKey, model.attr()) + } +} diff --git a/src/modules/Mutations.ts b/src/modules/Mutations.ts new file mode 100644 index 00000000..0e8e968f --- /dev/null +++ b/src/modules/Mutations.ts @@ -0,0 +1,13 @@ +import { Records } from '../data/Data' +import State from './State' + +/** + * Commit `insert` change to the store. + */ +function insert(state: State, records: Records): void { + state.data = { ...state.data, ...records } +} + +export default { + insert +} diff --git a/src/query/Query.ts b/src/query/Query.ts new file mode 100644 index 00000000..7b4c2697 --- /dev/null +++ b/src/query/Query.ts @@ -0,0 +1,57 @@ +import { Store } from 'vuex' +import { Record, Collection } from '../data/Data' +import Model from '../model/Model' +import Constructor from '../model/Constructor' +import Connection from '../connection/Connection' + +export default class Query { + /** + * The store instance. + */ + store: Store + + /** + * The model object. + */ + model: Constructor + + /** + * Create a new query instance. + */ + constructor(store: Store, model: Constructor) { + this.store = store + this.model = model + } + + /** + * Create a new connection instance. + */ + connection(): Connection { + return new Connection(this.store, this.model) + } + + /** + * Insert the given record to the store. + */ + async insert(records: Record[]): Promise> { + const models = this.hydrateRecords(records) + + this.connection().insert(this.dehydrateModels(models)) + + return models + } + + /** + * Instantiate new models with the given collection of records. + */ + private hydrateRecords(records: Record[]): Collection { + return records.map((record) => new this.model(record)) as any + } + + /** + * Convert all models into the plain record. + */ + private dehydrateModels(models: Collection): Record[] { + return models.map((model) => model.$getAttributes()) + } +} diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts new file mode 100644 index 00000000..29017b38 --- /dev/null +++ b/src/repository/Repository.ts @@ -0,0 +1,130 @@ +import { Store } from 'vuex' +import * as Data from '../data/Data' +import Model from '../model/Model' +import Constructor from '../model/Constructor' +import Interpretation from '../interpretation/Interpretation' +import Query from '../query/Query' + +interface CollectionPromises { + indexes: string[] + promises: Promise>[] +} + +export default class Repository { + /** + * The store instance. + */ + store: Store + + /** + * The model object. + */ + model: Constructor + + /** + * Create a new repository instance. + */ + constructor(store: Store, model: Constructor) { + this.store = store + this.model = model + } + + /** + * Create a new repository instance for the given entity. + */ + newRepository(entity: string): Repository { + const model = this.store.$database.getModel(entity) + + return new Repository(this.store, model) as Repository + } + + /** + * Create a new interpretation instance. + */ + interpretation(): Interpretation { + return new Interpretation(this.store, this.model) + } + + /** + * Create a new query instance. + */ + query(): Query { + return new Query(this.store, this.model) + } + + /** + * Insert the given record to the store. + */ + async insert(record: Data.Record | Data.Record[]): Promise { + const normalizedData = this.interpret(record) + + const { indexes, promises } = this.createCollectionPromises(normalizedData) + + return this.resolveCollectionPromises(indexes, promises) + } + + /** + * Insert the given normalized data. + */ + private insertNormalizedData( + records: Data.Records + ): Promise> { + return this.query().insert(this.mapNormalizedData(records)) + } + + /** + * Create collection promises for the given normalized data. + */ + private createCollectionPromises( + data: Data.NormalizedData + ): CollectionPromises { + const indexes: string[] = [] + const promises: Promise>[] = [] + + for (const entity in data) { + const records = data[entity] + const repository = this.newRepository(entity) + + indexes.push(entity) + promises.push(repository.insertNormalizedData(records)) + } + + return { indexes, promises } + } + + /** + * Resolve all collection promises and create a new collections object. + */ + private async resolveCollectionPromises( + indexes: string[], + promises: Promise>[] + ): Promise { + return (await Promise.all(promises)).reduce( + (collections, collection, index) => { + collections[indexes[index]] = collection + return collections + }, + {} + ) + } + + /** + * Convert normalized data into an array of records. + */ + private mapNormalizedData(records: Data.Records): Data.Record[] { + const items = [] as Data.Record[] + + for (const id in records) { + items.push(records[id]) + } + + return items + } + + /** + * Normalize the given record. + */ + private interpret(records: Data.Record | Data.Record[]): Data.NormalizedData { + return this.interpretation().process(records) + } +} diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts new file mode 100644 index 00000000..ceaabee6 --- /dev/null +++ b/src/schema/Schema.ts @@ -0,0 +1,48 @@ +import { schema as Normalizr } from 'normalizr' +import { Record } from '../data/Data' +import Model from '../model/Model' +import Constructor from '../model/Constructor' + +export default class Schema { + /** + * The model instance. + */ + model: Constructor + + /** + * Create a new schema instance. + */ + constructor(model: Constructor) { + this.model = model + } + + /** + * Create a single schema. + */ + one(): Normalizr.Entity { + const schema = this.newEntity() + + return schema + } + + /** + * Create a new normalizr entity. + */ + private newEntity(): Normalizr.Entity { + const entity = this.model.entity + const idAttribute = this.idArrtibute.bind(this) + + return new Normalizr.Entity(entity, {}, { idAttribute }) + } + + /** + * The id attribute option for the normalizr entity. + */ + private idArrtibute( + record: Record, + _parentRecord: Record, + _key: string + ): string { + return this.model.getIndexId(record) + } +} diff --git a/src/store/Store.ts b/src/store/Store.ts index 3d459780..b14839ff 100644 --- a/src/store/Store.ts +++ b/src/store/Store.ts @@ -1,5 +1,8 @@ import { Store, Plugin } from 'vuex' import Database from '../database/Database' +import Model from '../model/Model' +import Constructor from '../model/Constructor' +import Repository from '../repository/Repository' export interface Options { namespace?: string @@ -35,6 +38,8 @@ function mixin( ): void { connectDatabase(store, database, options) + mixinRepoFunction(store) + startDatabase(store) } @@ -57,3 +62,14 @@ function connectDatabase( function startDatabase(store: Store): void { store.$database.start() } + +/** + * Mixin repo function to the store. + */ +function mixinRepoFunction(store: Store): void { + store.$repo = function( + model: Constructor + ): Repository { + return new Repository(this, model) + } +} diff --git a/src/support/Utils.ts b/src/support/Utils.ts new file mode 100644 index 00000000..d7436063 --- /dev/null +++ b/src/support/Utils.ts @@ -0,0 +1,20 @@ +export function isArray(value: any): value is any[] { + return Array.isArray(value) +} + +/** + * Check if the given array or object is empty. + */ +export function isEmpty(collection: any[] | object): boolean { + return size(collection) === 0 +} + +/** + * Gets the size of collection by returning its length for array-like values + * or the number of own enumerable string keyed properties for objects. + */ +export function size(collection: any[] | object): number { + return isArray(collection) + ? collection.length + : Object.keys(collection).length +} diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index c5ddf026..00000000 --- a/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type ConstructorOf = { new (...args: any[]): C } diff --git a/src/types/vuex.d.ts b/src/types/vuex.d.ts index 06b8086d..00219ae5 100644 --- a/src/types/vuex.d.ts +++ b/src/types/vuex.d.ts @@ -1,4 +1,7 @@ import Database from '../database/Database' +import Model from '../model/Model' +import Constructor from '../model/Constructor' +import Repository from '../repository/Repository' declare module 'vuex' { interface Store { @@ -6,5 +9,10 @@ declare module 'vuex' { * The database instance. */ $database: Database + + /** + * Get a new repository instance for the given model. + */ + $repo(model: Constructor): Repository } } diff --git a/test/Helpers.ts b/test/Helpers.ts index c15f26f4..401af43a 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -1 +1,38 @@ -// ... +import Vue from 'vue' +import Vuex, { Store } from 'vuex' +import VuexORM, { Database, Model, RootState, Records } from '@/index' + +Vue.use(Vuex) + +interface Entities { + [name: string]: Records +} + +export function createStore(models: typeof Model[]): Store { + const database = new Database() + + models.forEach((model) => { + database.register(model) + }) + + return new Store({ + plugins: [VuexORM.install(database)], + strict: true + }) +} + +export function createState(entities: Entities): RootState { + const state = {} as RootState + + for (const entity in entities) { + state[entity] = { data: {} } + + state[entity].data = entities[entity] + } + + return state +} + +export function assertState(store: Store, entities: Entities): void { + expect(store.state.entities).toEqual(createState(entities)) +} diff --git a/test/feature/query/inserts_insert.spec.ts b/test/feature/query/inserts_insert.spec.ts new file mode 100644 index 00000000..3842ec88 --- /dev/null +++ b/test/feature/query/inserts_insert.spec.ts @@ -0,0 +1,44 @@ +import { createStore, assertState } from 'test/Helpers' +import { Model, Attr } from '@/index' + +describe('feature/query/inserts_insert', () => { + class User extends Model { + static entity = 'users' + @Attr() id: any + } + + it('inserts a record to the store', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({ id: 1 }) + + assertState(store, { + users: { + 1: { id: 1 } + } + }) + }) + + it('inserts records to the store', async () => { + const store = createStore([User]) + + await store.$repo(User).insert([{ id: 1 }, { id: 2 }]) + + assertState(store, { + users: { + 1: { id: 1 }, + 2: { id: 2 } + } + }) + }) + + it('does nothing if the given data is empty', async () => { + const store = createStore([User]) + + await store.$repo(User).insert({}) + assertState(store, { users: {} }) + + await store.$repo(User).insert([]) + assertState(store, { users: {} }) + }) +}) diff --git a/test/unit/VuexORM.spec.ts b/test/unit/VuexORM.spec.ts index be67e61e..99355ee8 100644 --- a/test/unit/VuexORM.spec.ts +++ b/test/unit/VuexORM.spec.ts @@ -7,11 +7,11 @@ describe('unit/VuexORM', () => { it('installs Vuex ORM to the store', () => { class User extends Model { - entity = 'users' + static entity = 'users' } class Post extends Model { - entity = 'posts' + static entity = 'posts' } const database = new Database() @@ -40,7 +40,7 @@ describe('unit/VuexORM', () => { it('can customize the namespace', () => { class User extends Model { - entity = 'users' + static entity = 'users' } const database = new Database() diff --git a/tsconfig.json b/tsconfig.json index 21adc64d..d675da58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, "baseUrl": ".", "rootDir": ".", "outDir": "dist", From 3f5b3e716e04968d20afb6f6be6209cab18cae09 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 15:53:49 +0900 Subject: [PATCH 25/56] feat: add more methods to the model constructor type --- src/model/Constructor.ts | 13 +++++++++++++ src/model/Model.ts | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/model/Constructor.ts b/src/model/Constructor.ts index cf438617..590fa84e 100644 --- a/src/model/Constructor.ts +++ b/src/model/Constructor.ts @@ -1,8 +1,21 @@ import { Record } from '../data/Data' +import * as Attributes from './attributes/Attributes' import Model from './Model' export default interface Constructor { entity: string + + primaryKey: string + new (...args: any[]): M + + setSchema( + this: M, + key: string, + attribute: Attributes.Attribute + ): M + + attr(this: M): Attributes.Attribute + getIndexId(record: Record): string } diff --git a/src/model/Model.ts b/src/model/Model.ts index 1ab7a9cc..7f82e162 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,7 +1,7 @@ import { Record } from '../data/Data' import * as Attributes from './attributes/Attributes' -interface Fields { +export interface Fields { [key: string]: Attributes.Attribute } From 4c4424a3c3c10b4786ca1e59b0a373e7590dc2cb Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 16:04:34 +0900 Subject: [PATCH 26/56] feat: add string attribute --- src/index.ts | 5 ++++- src/model/Model.ts | 9 ++++++++- src/model/attributes/Attributes.ts | 3 ++- src/model/attributes/types/String.ts | 11 +++++++++++ src/model/decorators/types/Str.ts | 13 +++++++++++++ test/feature/query/inserts_insert.spec.ts | 19 ++++++++++++------- 6 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 src/model/attributes/types/String.ts create mode 100644 src/model/decorators/types/Str.ts diff --git a/src/index.ts b/src/index.ts index 4ed0458c..1cb4f9da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { install, Options } from './store/Store' import Database from './database/Database' import Model from './model/Model' import Attr from './model/decorators/types/Attr' +import Str from './model/decorators/types/Str' import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' @@ -15,6 +16,7 @@ export { Database, Model, Attr, + Str, RootModule, RootState, Module, @@ -25,5 +27,6 @@ export default { install, Database, Model, - Attr + Attr, + Str } diff --git a/src/model/Model.ts b/src/model/Model.ts index 7f82e162..510840f8 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -45,10 +45,17 @@ export default class Model { /** * Create a new attr attribute class. */ - static attr(this: M): Attributes.Attribute { + static attr(this: M): Attributes.Attr { return new Attributes.Attr(this) } + /** + * Create a new string attribute class. + */ + static string(this: M): Attributes.String { + return new Attributes.String(this) + } + /** * Get the index id from the given record. An index ID is a value that * used as a index key for records within the store. diff --git a/src/model/attributes/Attributes.ts b/src/model/attributes/Attributes.ts index 0541515e..783aad1c 100644 --- a/src/model/attributes/Attributes.ts +++ b/src/model/attributes/Attributes.ts @@ -1,5 +1,6 @@ import Attribute from './Attribute' import Type from './types/Type' import Attr from './types/Attr' +import String from './types/String' -export { Attribute, Type, Attr } +export { Attribute, Type, Attr, String } diff --git a/src/model/attributes/types/String.ts b/src/model/attributes/types/String.ts new file mode 100644 index 00000000..adc13d8c --- /dev/null +++ b/src/model/attributes/types/String.ts @@ -0,0 +1,11 @@ +import Model from '../../Model' +import Type from './Type' + +export default class String extends Type { + /** + * Make the value for the attribute. + */ + make(value: any): string { + return value + } +} diff --git a/src/model/decorators/types/Str.ts b/src/model/decorators/types/Str.ts new file mode 100644 index 00000000..b29f6a77 --- /dev/null +++ b/src/model/decorators/types/Str.ts @@ -0,0 +1,13 @@ +import Model from '../../Model' +import PropertyDecorator from '../contracts/PropertyDecorator' + +/** + * Create a str decorator. + */ +export default function Str(): PropertyDecorator { + return (target, propertyKey) => { + const model = target.constructor as typeof Model + + model.setSchema(propertyKey, model.string()) + } +} diff --git a/test/feature/query/inserts_insert.spec.ts b/test/feature/query/inserts_insert.spec.ts index 3842ec88..d8966f39 100644 --- a/test/feature/query/inserts_insert.spec.ts +++ b/test/feature/query/inserts_insert.spec.ts @@ -1,20 +1,22 @@ import { createStore, assertState } from 'test/Helpers' -import { Model, Attr } from '@/index' +import { Model, Attr, Str } from '@/index' describe('feature/query/inserts_insert', () => { class User extends Model { static entity = 'users' - @Attr() id: any + + @Attr() id!: any + @Str() name!: string } it('inserts a record to the store', async () => { const store = createStore([User]) - await store.$repo(User).insert({ id: 1 }) + await store.$repo(User).insert({ id: 1, name: 'John Doe' }) assertState(store, { users: { - 1: { id: 1 } + 1: { id: 1, name: 'John Doe' } } }) }) @@ -22,12 +24,15 @@ describe('feature/query/inserts_insert', () => { it('inserts records to the store', async () => { const store = createStore([User]) - await store.$repo(User).insert([{ id: 1 }, { id: 2 }]) + await store.$repo(User).insert([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' } + ]) assertState(store, { users: { - 1: { id: 1 }, - 2: { id: 2 } + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' } } }) }) From 8a48bfdc486b269e57f20df2816e6864775ce4ce Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 16:39:39 +0900 Subject: [PATCH 27/56] feat: add string, number, and boolean attributes --- src/model/Model.ts | 27 ++++++++++++- src/model/attributes/Attributes.ts | 4 +- src/model/attributes/types/Boolean.ts | 44 +++++++++++++++++++++ src/model/attributes/types/Number.ts | 38 ++++++++++++++++++ src/model/attributes/types/String.ts | 23 ++++++++++- src/model/attributes/types/Type.ts | 29 +++++++++++++- src/model/decorators/types/Bool.ts | 26 ++++++++++++ src/model/decorators/types/Num.ts | 26 ++++++++++++ src/model/decorators/types/Str.ts | 17 +++++++- test/feature/query/inserts_insert.spec.ts | 2 +- test/unit/model/Model_Attrs_Boolean.spec.ts | 40 +++++++++++++++++++ test/unit/model/Model_Attrs_Number.spec.ts | 36 +++++++++++++++++ test/unit/model/Model_Attrs_String.spec.ts | 34 ++++++++++++++++ 13 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 src/model/attributes/types/Boolean.ts create mode 100644 src/model/attributes/types/Number.ts create mode 100644 src/model/decorators/types/Bool.ts create mode 100644 src/model/decorators/types/Num.ts create mode 100644 test/unit/model/Model_Attrs_Boolean.spec.ts create mode 100644 test/unit/model/Model_Attrs_Number.spec.ts create mode 100644 test/unit/model/Model_Attrs_String.spec.ts diff --git a/src/model/Model.ts b/src/model/Model.ts index 510840f8..181791f3 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -52,8 +52,31 @@ export default class Model { /** * Create a new string attribute class. */ - static string(this: M): Attributes.String { - return new Attributes.String(this) + static string( + this: M, + value: string | null + ): Attributes.String { + return new Attributes.String(this, value) + } + + /** + * Create a new number attribute class. + */ + static number( + this: M, + value: number | null + ): Attributes.Number { + return new Attributes.Number(this, value) + } + + /** + * Create a new boolean attribute class. + */ + static boolean( + this: M, + value: boolean | null + ): Attributes.Boolean { + return new Attributes.Boolean(this, value) } /** diff --git a/src/model/attributes/Attributes.ts b/src/model/attributes/Attributes.ts index 783aad1c..a203cc57 100644 --- a/src/model/attributes/Attributes.ts +++ b/src/model/attributes/Attributes.ts @@ -2,5 +2,7 @@ import Attribute from './Attribute' import Type from './types/Type' import Attr from './types/Attr' import String from './types/String' +import Number from './types/Number' +import Boolean from './types/Boolean' -export { Attribute, Type, Attr, String } +export { Attribute, Type, Attr, String, Number, Boolean } diff --git a/src/model/attributes/types/Boolean.ts b/src/model/attributes/types/Boolean.ts new file mode 100644 index 00000000..bc89fd23 --- /dev/null +++ b/src/model/attributes/types/Boolean.ts @@ -0,0 +1,44 @@ +import Model from '../../Model' +import Type from './Type' + +export default class Boolean extends Type { + /** + * Create a new boolean attribute. + */ + constructor(model: M, value: boolean | null) { + super(model, value) + } + + /** + * Make the value for the attribute. + */ + make(value: any): boolean | null { + if (value === undefined) { + return this.value + } + + if (typeof value === 'boolean') { + return value + } + + if (typeof value === 'string') { + if (value.length === 0) { + return false + } + + const int = parseInt(value, 0) + + return isNaN(int) ? true : !!int + } + + if (typeof value === 'number') { + return !!value + } + + if (value === null && this.isNullable) { + return value + } + + return false + } +} diff --git a/src/model/attributes/types/Number.ts b/src/model/attributes/types/Number.ts new file mode 100644 index 00000000..471a2fe9 --- /dev/null +++ b/src/model/attributes/types/Number.ts @@ -0,0 +1,38 @@ +import Model from '../../Model' +import Type from './Type' + +export default class Number extends Type { + /** + * Create a new number attribute. + */ + constructor(model: M, value: number | null) { + super(model, value) + } + + /** + * Make the value for the attribute. + */ + make(value: any): number | null { + if (value === undefined) { + return this.value + } + + if (typeof value === 'number') { + return value + } + + if (typeof value === 'string') { + return parseFloat(value) + } + + if (typeof value === 'boolean') { + return value ? 1 : 0 + } + + if (value === null && this.isNullable) { + return value + } + + return 0 + } +} diff --git a/src/model/attributes/types/String.ts b/src/model/attributes/types/String.ts index adc13d8c..a7e2f0d0 100644 --- a/src/model/attributes/types/String.ts +++ b/src/model/attributes/types/String.ts @@ -2,10 +2,29 @@ import Model from '../../Model' import Type from './Type' export default class String extends Type { + /** + * Create a new string attribute. + */ + constructor(model: M, value: string | null) { + super(model, value) + } + /** * Make the value for the attribute. */ - make(value: any): string { - return value + make(value: any): string | null { + if (value === undefined) { + return this.value + } + + if (typeof value === 'string') { + return value + } + + if (value === null && this.isNullable) { + return value + } + + return value + '' } } diff --git a/src/model/attributes/types/Type.ts b/src/model/attributes/types/Type.ts index 7c5a2546..03f0d76a 100644 --- a/src/model/attributes/types/Type.ts +++ b/src/model/attributes/types/Type.ts @@ -3,4 +3,31 @@ import Attribute from '../Attribute' export default abstract class Type extends Attribute< M -> {} +> { + /** + * The default value for the attribute. + */ + value: any + + /** + * Whether if the attribute accepts `null` value or not. + */ + isNullable: boolean = false + + /** + * Create a new string attribute. + */ + constructor(model: M, value: any = null) { + super(model) + this.value = value + } + + /** + * Set the nullable option to true. + */ + nullable(): this { + this.isNullable = true + + return this + } +} diff --git a/src/model/decorators/types/Bool.ts b/src/model/decorators/types/Bool.ts new file mode 100644 index 00000000..4645b8f2 --- /dev/null +++ b/src/model/decorators/types/Bool.ts @@ -0,0 +1,26 @@ +import Model from '../../Model' +import PropertyDecorator from '../contracts/PropertyDecorator' + +interface Options { + nullable?: boolean +} + +/** + * Create a bool decorator. + */ +export default function Bool( + value: boolean | null, + options: Options = {} +): PropertyDecorator { + return (target, propertyKey) => { + const model = target.constructor as typeof Model + + const attr = model.boolean(value) + + if (options.nullable) { + attr.nullable() + } + + model.setSchema(propertyKey, attr) + } +} diff --git a/src/model/decorators/types/Num.ts b/src/model/decorators/types/Num.ts new file mode 100644 index 00000000..bddc9cd4 --- /dev/null +++ b/src/model/decorators/types/Num.ts @@ -0,0 +1,26 @@ +import Model from '../../Model' +import PropertyDecorator from '../contracts/PropertyDecorator' + +interface Options { + nullable?: boolean +} + +/** + * Create a num decorator. + */ +export default function Num( + value: number | null, + options: Options = {} +): PropertyDecorator { + return (target, propertyKey) => { + const model = target.constructor as typeof Model + + const attr = model.number(value) + + if (options.nullable) { + attr.nullable() + } + + model.setSchema(propertyKey, attr) + } +} diff --git a/src/model/decorators/types/Str.ts b/src/model/decorators/types/Str.ts index b29f6a77..f9da3c94 100644 --- a/src/model/decorators/types/Str.ts +++ b/src/model/decorators/types/Str.ts @@ -1,13 +1,26 @@ import Model from '../../Model' import PropertyDecorator from '../contracts/PropertyDecorator' +interface Options { + nullable?: boolean +} + /** * Create a str decorator. */ -export default function Str(): PropertyDecorator { +export default function Str( + value: string | null, + options: Options = {} +): PropertyDecorator { return (target, propertyKey) => { const model = target.constructor as typeof Model - model.setSchema(propertyKey, model.string()) + const attr = model.string(value) + + if (options.nullable) { + attr.nullable() + } + + model.setSchema(propertyKey, attr) } } diff --git a/test/feature/query/inserts_insert.spec.ts b/test/feature/query/inserts_insert.spec.ts index d8966f39..e2d7c0e5 100644 --- a/test/feature/query/inserts_insert.spec.ts +++ b/test/feature/query/inserts_insert.spec.ts @@ -6,7 +6,7 @@ describe('feature/query/inserts_insert', () => { static entity = 'users' @Attr() id!: any - @Str() name!: string + @Str('') name!: string } it('inserts a record to the store', async () => { diff --git a/test/unit/model/Model_Attrs_Boolean.spec.ts b/test/unit/model/Model_Attrs_Boolean.spec.ts new file mode 100644 index 00000000..2ae23ccf --- /dev/null +++ b/test/unit/model/Model_Attrs_Boolean.spec.ts @@ -0,0 +1,40 @@ +import Bool from '@/model/decorators/types/Bool' +import Model from '@/model/Model' + +describe('unit/model/Model_Attrs_Boolean', () => { + it('casts the value to `Boolean` when instantiating the model', () => { + class User extends Model { + static entity = 'users' + + @Bool(true) + bool!: number + } + + expect(new User({}).bool).toBe(true) + expect(new User({ bool: '' }).bool).toBe(false) + expect(new User({ bool: 'string' }).bool).toBe(true) + expect(new User({ bool: '0' }).bool).toBe(false) + expect(new User({ bool: 0 }).bool).toBe(false) + expect(new User({ bool: 1 }).bool).toBe(true) + expect(new User({ bool: true }).bool).toBe(true) + expect(new User({ bool: null }).bool).toBe(false) + }) + + it('accepts `null` when the `nullable` option is set', () => { + class User extends Model { + static entity = 'users' + + @Bool(null, { nullable: true }) + bool!: boolean | null + } + + expect(new User({}).bool).toBe(null) + expect(new User({ bool: '' }).bool).toBe(false) + expect(new User({ bool: 'string' }).bool).toBe(true) + expect(new User({ bool: '0' }).bool).toBe(false) + expect(new User({ bool: 0 }).bool).toBe(false) + expect(new User({ bool: 1 }).bool).toBe(true) + expect(new User({ bool: true }).bool).toBe(true) + expect(new User({ bool: null }).bool).toBe(null) + }) +}) diff --git a/test/unit/model/Model_Attrs_Number.spec.ts b/test/unit/model/Model_Attrs_Number.spec.ts new file mode 100644 index 00000000..58a1da71 --- /dev/null +++ b/test/unit/model/Model_Attrs_Number.spec.ts @@ -0,0 +1,36 @@ +import Num from '@/model/decorators/types/Num' +import Model from '@/model/Model' + +describe('unit/model/Model_Attrs_Number', () => { + it('casts the value to `Number` when instantiating the model', () => { + class User extends Model { + static entity = 'users' + + @Num(0) + num!: number + } + + expect(new User({}).num).toBe(0) + expect(new User({ num: 1 }).num).toBe(1) + expect(new User({ num: '2' }).num).toBe(2) + expect(new User({ num: true }).num).toBe(1) + expect(new User({ num: false }).num).toBe(0) + expect(new User({ num: null }).num).toBe(0) + }) + + it('accepts `null` when the `nullable` option is set', () => { + class User extends Model { + static entity = 'users' + + @Num(null, { nullable: true }) + num!: number | null + } + + expect(new User({}).num).toBe(null) + expect(new User({ num: 1 }).num).toBe(1) + expect(new User({ num: '2' }).num).toBe(2) + expect(new User({ num: true }).num).toBe(1) + expect(new User({ num: false }).num).toBe(0) + expect(new User({ num: null }).num).toBe(null) + }) +}) diff --git a/test/unit/model/Model_Attrs_String.spec.ts b/test/unit/model/Model_Attrs_String.spec.ts new file mode 100644 index 00000000..b2b29ad0 --- /dev/null +++ b/test/unit/model/Model_Attrs_String.spec.ts @@ -0,0 +1,34 @@ +import Str from '@/model/decorators/types/Str' +import Model from '@/model/Model' + +describe('unit/model/Model_Attrs_String', () => { + it('casts the value to `String` when instantiating the model', () => { + class User extends Model { + static entity = 'users' + + @Str('default') + str!: string + } + + expect(new User({}).str).toBe('default') + expect(new User({ str: 'value' }).str).toBe('value') + expect(new User({ str: 1 }).str).toBe('1') + expect(new User({ str: true }).str).toBe('true') + expect(new User({ str: null }).str).toBe('null') + }) + + it('accepts `null` when the `nullable` option is set', () => { + class User extends Model { + static entity = 'users' + + @Str(null, { nullable: true }) + str!: string | null + } + + expect(new User({}).str).toBe(null) + expect(new User({ str: 'value' }).str).toBe('value') + expect(new User({ str: 1 }).str).toBe('1') + expect(new User({ str: true }).str).toBe('true') + expect(new User({ str: null }).str).toBe(null) + }) +}) From 715c1f0e0f2467e25d4aa832517867ad804ede3c Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 17:26:56 +0900 Subject: [PATCH 28/56] feat: add delete all feature --- src/connection/Connection.ts | 19 +++++++++- src/modules/Mutations.ts | 18 +++++++++- src/query/Query.ts | 35 ++++++++++++++++++- src/repository/Repository.ts | 7 ++++ test/Helpers.ts | 10 +++++- test/feature/query/deletes_delete_all.spec.ts | 29 +++++++++++++++ 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 test/feature/query/deletes_delete_all.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index 59c38d2d..c2334cb6 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -22,10 +22,20 @@ export default class Connection { this.model = model } + /** + * Get all existing records. + */ + get(): Records { + const namespace = this.store.$database.connection + const entity = this.model.entity + + return this.store.state[namespace][entity].data + } + /** * Commit the store mutation. */ - private commit(name: string, payload: any): void { + private commit(name: string, payload?: any): void { const namespace = this.store.$database.connection const entity = this.model.entity @@ -48,4 +58,11 @@ export default class Connection { return carry }, {}) } + + /** + * Commit `deleteAll` change to the store. + */ + deleteAll(): void { + this.commit('deleteAll') + } } diff --git a/src/modules/Mutations.ts b/src/modules/Mutations.ts index 0e8e968f..56d61c88 100644 --- a/src/modules/Mutations.ts +++ b/src/modules/Mutations.ts @@ -1,6 +1,13 @@ import { Records } from '../data/Data' import State from './State' +/** + * Generic mutation. + */ +function mutate(state: State, callback: (state: State) => void): void { + callback(state) +} + /** * Commit `insert` change to the store. */ @@ -8,6 +15,15 @@ function insert(state: State, records: Records): void { state.data = { ...state.data, ...records } } +/** + * Commit `deleteAll` change to the store. + */ +function destroyAll(state: State): void { + state.data = {} +} + export default { - insert + mutate, + insert, + deleteAll: destroyAll } diff --git a/src/query/Query.ts b/src/query/Query.ts index 7b4c2697..cded9a2e 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -30,6 +30,21 @@ export default class Query { return new Connection(this.store, this.model) } + /** + * Get all existing records and hydrate them all. + */ + getModels(): Collection { + const records = this.connection().get() + + const collection = [] as Collection + + for (const id in records) { + collection.push(this.hydrateRecord(records[id])) + } + + return collection + } + /** * Insert the given record to the store. */ @@ -41,11 +56,29 @@ export default class Query { return models } + /** + * Delete all records in the store. + */ + async deleteAll(): Promise> { + const models = this.getModels() + + this.connection().deleteAll() + + return models + } + + /** + * Instantiate new models with the given record. + */ + private hydrateRecord(record: Record): M { + return new this.model(record) + } + /** * Instantiate new models with the given collection of records. */ private hydrateRecords(records: Record[]): Collection { - return records.map((record) => new this.model(record)) as any + return records.map((record) => this.hydrateRecord(record)) } /** diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 29017b38..f3a27304 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -127,4 +127,11 @@ export default class Repository { private interpret(records: Data.Record | Data.Record[]): Data.NormalizedData { return this.interpretation().process(records) } + + /** + * Delete all records in the store. + */ + deleteAll(): Promise> { + return this.query().deleteAll() + } } diff --git a/test/Helpers.ts b/test/Helpers.ts index 401af43a..dbe984bd 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -1,6 +1,6 @@ import Vue from 'vue' import Vuex, { Store } from 'vuex' -import VuexORM, { Database, Model, RootState, Records } from '@/index' +import VuexORM, { Database, Model, RootState, State, Records } from '@/index' Vue.use(Vuex) @@ -33,6 +33,14 @@ export function createState(entities: Entities): RootState { return state } +export function fillState(store: Store, entities: Entities): void { + for (const entity in entities) { + store.commit(`entities/${entity}/mutate`, (state: State): void => { + state.data = entities[entity] + }) + } +} + export function assertState(store: Store, entities: Entities): void { expect(store.state.entities).toEqual(createState(entities)) } diff --git a/test/feature/query/deletes_delete_all.spec.ts b/test/feature/query/deletes_delete_all.spec.ts new file mode 100644 index 00000000..15cb7822 --- /dev/null +++ b/test/feature/query/deletes_delete_all.spec.ts @@ -0,0 +1,29 @@ +import { createStore, fillState, assertState } from 'test/Helpers' +import { Model, Attr, Str } from '@/index' + +describe('feature/query/deletes_delete_all', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + it('deletes all records in the store', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + await store.$repo(User).deleteAll() + + assertState(store, { + users: {} + }) + }) +}) From a1ca9ee9d1862487bc59179c1d2cc95ae849ea34 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 17:45:37 +0900 Subject: [PATCH 29/56] feat: add all feature --- src/query/Query.ts | 10 +++++- src/repository/Repository.ts | 7 +++++ test/Helpers.ts | 28 ++++++++++++++++- test/feature/query/retrieves_all.spec.ts | 40 ++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 test/feature/query/retrieves_all.spec.ts diff --git a/src/query/Query.ts b/src/query/Query.ts index cded9a2e..feb56a77 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -31,7 +31,15 @@ export default class Query { } /** - * Get all existing records and hydrate them all. + * Get records by processing the whole query chain. + */ + get(): Collection { + return this.getModels() + } + + /** + * Get all existing records and hydrate them all. The difference with `get` + * method is that this method will not process any query chain. */ getModels(): Collection { const records = this.connection().get() diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index f3a27304..4cecbbfe 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -52,6 +52,13 @@ export default class Repository { return new Query(this.store, this.model) } + /** + * Get all models from the store. + */ + all(): Data.Collection { + return this.query().get() + } + /** * Insert the given record to the store. */ diff --git a/test/Helpers.ts b/test/Helpers.ts index dbe984bd..4bb5b3f3 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -1,6 +1,14 @@ import Vue from 'vue' import Vuex, { Store } from 'vuex' -import VuexORM, { Database, Model, RootState, State, Records } from '@/index' +import VuexORM, { + Database, + Model, + RootState, + State, + Record, + Records, + Collection +} from '@/index' Vue.use(Vuex) @@ -44,3 +52,21 @@ export function fillState(store: Store, entities: Entities): void { export function assertState(store: Store, entities: Entities): void { expect(store.state.entities).toEqual(createState(entities)) } + +export function assertModels( + models: Collection, + record: Record[] +): void { + models.forEach((model, index) => { + expect(model.$getAttributes()).toEqual(record[index]) + }) +} + +export function assertInstanceOf( + collection: Collection, + model: typeof Model +): void { + collection.forEach((item) => { + expect(item).toBeInstanceOf(model) + }) +} diff --git a/test/feature/query/retrieves_all.spec.ts b/test/feature/query/retrieves_all.spec.ts new file mode 100644 index 00000000..d961b6ea --- /dev/null +++ b/test/feature/query/retrieves_all.spec.ts @@ -0,0 +1,40 @@ +import { + createStore, + fillState, + assertInstanceOf, + assertModels +} from 'test/Helpers' +import { Model, Attr, Str } from '@/index' + +describe('feature/query/retrieves_all', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + it('retrieves all records from the store', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + const users = store.$repo(User).all() + + const expected = [ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + { id: 3, name: 'Johnny Doe' } + ] + + expect(users.length).toBe(3) + assertInstanceOf(users, User) + assertModels(users, expected) + }) +}) From 1eb2182510cb249f4dbd502bbf46a80a970b8bd2 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 17:55:35 +0900 Subject: [PATCH 30/56] feat: add find feature --- src/connection/Connection.ts | 10 +++++ src/query/Query.ts | 11 ++++- src/repository/Repository.ts | 7 ++++ test/feature/query/retrieves_find.spec.ts | 49 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/feature/query/retrieves_find.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index c2334cb6..eddaae00 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -32,6 +32,16 @@ export default class Connection { return this.store.state[namespace][entity].data } + /** + * Find a record with the given id. + */ + find(id: string | number): Record | null { + const namespace = this.store.$database.connection + const entity = this.model.entity + + return this.store.state[namespace][entity].data[id] ?? null + } + /** * Commit the store mutation. */ diff --git a/src/query/Query.ts b/src/query/Query.ts index feb56a77..65b6de0c 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -1,5 +1,5 @@ import { Store } from 'vuex' -import { Record, Collection } from '../data/Data' +import { Record, Item, Collection } from '../data/Data' import Model from '../model/Model' import Constructor from '../model/Constructor' import Connection from '../connection/Connection' @@ -37,6 +37,15 @@ export default class Query { return this.getModels() } + /** + * Find the model with the given id. + */ + find(id: string | number): Item { + const record = this.connection().find(id) + + return record ? this.hydrateRecord(record) : null + } + /** * Get all existing records and hydrate them all. The difference with `get` * method is that this method will not process any query chain. diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 4cecbbfe..ed3d49de 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -59,6 +59,13 @@ export default class Repository { return this.query().get() } + /** + * Find the model with the given id. + */ + find(id: string | number): Data.Item { + return this.query().find(id) + } + /** * Insert the given record to the store. */ diff --git a/test/feature/query/retrieves_find.spec.ts b/test/feature/query/retrieves_find.spec.ts new file mode 100644 index 00000000..e1feb482 --- /dev/null +++ b/test/feature/query/retrieves_find.spec.ts @@ -0,0 +1,49 @@ +import { + createStore, + fillState +} from 'test/Helpers' +import { Model, Attr, Str } from '@/index' + +describe('feature/query/retrieves_find', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + it('can find a record by id', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + const user = store.$repo(User).find(2) + + const expected = { id: 2, name: 'Jane Doe' } + + expect(user).toBeInstanceOf(User) + expect(user).toEqual(expected) + }) + + it('returns `null` if the reocrd is not found', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + const user = store.$repo(User).find(4) + + expect(user).toBe(null) + }) +}) From 8f383166d70d15284a04ca526a7566713c7e761a Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 17:57:31 +0900 Subject: [PATCH 31/56] style: fix linter error --- test/feature/query/retrieves_find.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/feature/query/retrieves_find.spec.ts b/test/feature/query/retrieves_find.spec.ts index e1feb482..8940d6dd 100644 --- a/test/feature/query/retrieves_find.spec.ts +++ b/test/feature/query/retrieves_find.spec.ts @@ -1,7 +1,4 @@ -import { - createStore, - fillState -} from 'test/Helpers' +import { createStore, fillState } from 'test/Helpers' import { Model, Attr, Str } from '@/index' describe('feature/query/retrieves_find', () => { From 0cf7430b92ff68328f1f8d64761bc512faef578f Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 21:05:00 +0900 Subject: [PATCH 32/56] feat: add `where` and `orWhere` filter --- src/index.ts | 8 +- src/query/Query.ts | 80 ++++++++++++++++++- src/query/options/Options.ts | 10 +++ src/repository/Repository.ts | 16 +++- src/support/Utils.ts | 21 +++++ test/feature/query/retrieves_where.spec.ts | 67 ++++++++++++++++ test/feature/query/retrieves_where_or.spec.ts | 67 ++++++++++++++++ 7 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 src/query/options/Options.ts create mode 100644 test/feature/query/retrieves_where.spec.ts create mode 100644 test/feature/query/retrieves_where_or.spec.ts diff --git a/src/index.ts b/src/index.ts index 1cb4f9da..49779941 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import Database from './database/Database' import Model from './model/Model' import Attr from './model/decorators/types/Attr' import Str from './model/decorators/types/Str' +import Num from './model/decorators/types/Num' +import Bool from './model/decorators/types/Bool' import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' @@ -17,6 +19,8 @@ export { Model, Attr, Str, + Num, + Bool, RootModule, RootState, Module, @@ -28,5 +32,7 @@ export default { Database, Model, Attr, - Str + Str, + Num, + Bool } diff --git a/src/query/Query.ts b/src/query/Query.ts index 65b6de0c..1f73e595 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -1,8 +1,10 @@ import { Store } from 'vuex' +import { isEmpty, groupBy } from '../support/Utils' import { Record, Item, Collection } from '../data/Data' import Model from '../model/Model' import Constructor from '../model/Constructor' import Connection from '../connection/Connection' +import * as Options from './options/Options' export default class Query { /** @@ -15,6 +17,11 @@ export default class Query { */ model: Constructor + /** + * The where constraints for the query. + */ + wheres: Options.Where[] = [] + /** * Create a new query instance. */ @@ -31,10 +38,28 @@ export default class Query { } /** - * Get records by processing the whole query chain. + * Add a basic where clause to the query. + */ + where(field: string, value: any): this { + this.wheres.push({ field, value, boolean: 'and' }) + + return this + } + + /** + * Add an "or where" clause to the query. + */ + orWhere(field: string, value: any): Query { + this.wheres.push({ field, value, boolean: 'or' }) + + return this + } + + /** + * Retrieve models by processing whole query chain. */ get(): Collection { - return this.getModels() + return this.select() } /** @@ -47,8 +72,8 @@ export default class Query { } /** - * Get all existing records and hydrate them all. The difference with `get` - * method is that this method will not process any query chain. + * Get all models from the state. The difference with the `get` is that this + * method will not process any query chain. It'll always retrieve all models. */ getModels(): Collection { const records = this.connection().get() @@ -62,6 +87,53 @@ export default class Query { return collection } + /** + * Retrieve models by processing all filters set to the query chain. + */ + select(): Collection { + let models = this.getModels() + + models = this.filterWhere(models) + + return models + } + + /** + * Filter the given collection by the registered where clause. + */ + private filterWhere(models: Collection): Collection { + if (isEmpty(this.wheres)) { + return models + } + + const comparator = this.getWhereComparator() + + return models.filter((model) => comparator(model)) + } + + /** + * Get comparator for the where clause. + */ + private getWhereComparator(): (model: M) => boolean { + const { and, or } = groupBy(this.wheres, (where) => where.boolean) + + return (model) => { + const results: boolean[] = [] + + and && results.push(and.every((w) => this.whereComparator(model, w))) + or && results.push(or.some((w) => this.whereComparator(model, w))) + + return results.indexOf(true) !== -1 + } + } + + /** + * The function to compare where clause to the given model. + */ + private whereComparator(model: M, where: Options.Where): boolean { + return model[where.field] === where.value + } + /** * Insert the given record to the store. */ diff --git a/src/query/options/Options.ts b/src/query/options/Options.ts new file mode 100644 index 00000000..dc9af66e --- /dev/null +++ b/src/query/options/Options.ts @@ -0,0 +1,10 @@ +export interface Where { + field: string | number + value: any + boolean: 'and' | 'or' +} + +export interface WhereGroup { + and?: Where[] + or?: Where[] +} diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index ed3d49de..a0d7b4e4 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -35,7 +35,7 @@ export default class Repository { newRepository(entity: string): Repository { const model = this.store.$database.getModel(entity) - return new Repository(this.store, model) as Repository + return new Repository(this.store, model) as any } /** @@ -52,6 +52,20 @@ export default class Repository { return new Query(this.store, this.model) } + /** + * Add a basic where clause to the query. + */ + where(field: string, value: any): Query { + return this.query().where(field, value) + } + + /** + * Add an "or where" clause to the query. + */ + orWhere(field: string, value: any): Query { + return this.query().orWhere(field, value) + } + /** * Get all models from the store. */ diff --git a/src/support/Utils.ts b/src/support/Utils.ts index d7436063..81c70b50 100644 --- a/src/support/Utils.ts +++ b/src/support/Utils.ts @@ -18,3 +18,24 @@ export function size(collection: any[] | object): number { ? collection.length : Object.keys(collection).length } + +/** + * Creates an object composed of keys generated from the results of running + * each element of collection through iteratee. + */ +export function groupBy( + collection: T[], + iteratee: (record: T) => string +): { [key: string]: T[] } { + return collection.reduce((records, record) => { + const key = iteratee(record) + + if (records[key] === undefined) { + records[key] = [] + } + + records[key].push(record) + + return records + }, {}) +} diff --git a/test/feature/query/retrieves_where.spec.ts b/test/feature/query/retrieves_where.spec.ts new file mode 100644 index 00000000..24d8a92c --- /dev/null +++ b/test/feature/query/retrieves_where.spec.ts @@ -0,0 +1,67 @@ +import { + createStore, + fillState, + assertInstanceOf, + assertModels +} from 'test/Helpers' +import { Model, Attr, Str, Num } from '@/index' + +describe('feature/query/retrieves_where', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + @Num(0) age!: number + } + + it('can filter the query by the `where` clause', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 30 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + const users = store + .$repo(User) + .where('age', 30) + .get() + + const expected = [ + { id: 1, name: 'John Doe', age: 30 }, + { id: 2, name: 'Jane Doe', age: 30 } + ] + + expect(users.length).toBe(2) + assertInstanceOf(users, User) + assertModels(users, expected) + }) + + it('can chain multiple `where` clause', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 30 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + const users = store + .$repo(User) + .where('name', 'John Doe') + .where('age', 30) + .get() + + const expected = [{ id: 1, name: 'John Doe', age: 30 }] + + expect(users.length).toBe(1) + assertInstanceOf(users, User) + assertModels(users, expected) + }) +}) diff --git a/test/feature/query/retrieves_where_or.spec.ts b/test/feature/query/retrieves_where_or.spec.ts new file mode 100644 index 00000000..f8308554 --- /dev/null +++ b/test/feature/query/retrieves_where_or.spec.ts @@ -0,0 +1,67 @@ +import { + createStore, + fillState, + assertInstanceOf, + assertModels +} from 'test/Helpers' +import { Model, Attr, Str, Num } from '@/index' + +describe('feature/query/retrieves_where', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + @Num(0) age!: number + } + + it('can filter query by `or where` clause', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + const users = store + .$repo(User) + .where('name', 'John Doe') + .orWhere('age', 30) + .get() + + const expected = [ + { id: 1, name: 'John Doe', age: 40 }, + { id: 2, name: 'Jane Doe', age: 30 } + ] + + expect(users.length).toBe(2) + assertInstanceOf(users, User) + assertModels(users, expected) + }) + + it('the "or where" clause can be used alone', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + const users = store + .$repo(User) + .orWhere('age', 30) + .get() + + const expected = [{ id: 2, name: 'Jane Doe', age: 30 }] + + expect(users.length).toBe(1) + assertInstanceOf(users, User) + assertModels(users, expected) + }) +}) From 9fd73f19373eac2e8f0ad2ed3f7210498e68672b Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 21:35:06 +0900 Subject: [PATCH 33/56] feat: add `delete` feature --- src/connection/Connection.ts | 7 +++ src/model/Model.ts | 9 +++- src/modules/Mutations.ts | 16 ++++++ src/query/Query.ts | 18 +++++++ test/feature/query/deletes_delete.spec.ts | 59 +++++++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 test/feature/query/deletes_delete.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index eddaae00..f3dfa0a1 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -69,6 +69,13 @@ export default class Connection { }, {}) } + /** + * Commit `delete` change to the store. + */ + delete(ids: string[]): void { + this.commit('delete', ids) + } + /** * Commit `deleteAll` change to the store. */ diff --git a/src/model/Model.ts b/src/model/Model.ts index 181791f3..bd95143e 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -84,7 +84,7 @@ export default class Model { * used as a index key for records within the store. */ static getIndexId(record: Record): string { - return record[this.primaryKey] + return String(record[this.primaryKey]) } /** @@ -118,6 +118,13 @@ export default class Model { return this } + /** + * Get the index id value for the model. + */ + $getIndexId(): string { + return this.$self.getIndexId(this) + } + /** * Get the serialized model attributes. */ diff --git a/src/modules/Mutations.ts b/src/modules/Mutations.ts index 56d61c88..758f0463 100644 --- a/src/modules/Mutations.ts +++ b/src/modules/Mutations.ts @@ -15,6 +15,21 @@ function insert(state: State, records: Records): void { state.data = { ...state.data, ...records } } +/** + * Commit `delete` change to the store. + */ +function destroy(state: State, ids: string[]): void { + const data = {} + + for (const id in state.data) { + if (!ids.includes(id)) { + data[id] = state.data[id] + } + } + + state.data = data +} + /** * Commit `deleteAll` change to the store. */ @@ -25,5 +40,6 @@ function destroyAll(state: State): void { export default { mutate, insert, + delete: destroy, deleteAll: destroyAll } diff --git a/src/query/Query.ts b/src/query/Query.ts index 1f73e595..fd9f2b55 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -145,6 +145,17 @@ export default class Query { return models } + /** + * Delete records that match the query chain. + */ + async delete(): Promise> { + const models = this.get() + + this.connection().delete(this.getIdsFromCollection(models)) + + return models + } + /** * Delete all records in the store. */ @@ -176,4 +187,11 @@ export default class Query { private dehydrateModels(models: Collection): Record[] { return models.map((model) => model.$getAttributes()) } + + /** + * Get an array of ids from the given collection. + */ + private getIdsFromCollection(models: Collection): string[] { + return models.map((model) => model.$getIndexId()) + } } diff --git a/test/feature/query/deletes_delete.spec.ts b/test/feature/query/deletes_delete.spec.ts new file mode 100644 index 00000000..8a4bbe15 --- /dev/null +++ b/test/feature/query/deletes_delete.spec.ts @@ -0,0 +1,59 @@ +import { createStore, fillState, assertState } from 'test/Helpers' +import { Model, Attr, Str } from '@/index' + +describe('feature/query/deletes_delete', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + it('deletes a record specified by the where clause.', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + await store + .$repo(User) + .where('name', 'Jane Doe') + .delete() + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + }) + + it('can delete multiple records specified by the where clause.', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + await store + .$repo(User) + .where('name', 'Jane Doe') + .orWhere('name', 'Johnny Doe') + .delete() + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + } + }) + }) +}) From fe0c6700d2e87efcf97aa2d1c580657f0ed75873 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 21:36:14 +0900 Subject: [PATCH 34/56] chore: remove trailing period from test cases --- test/feature/query/deletes_delete.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/feature/query/deletes_delete.spec.ts b/test/feature/query/deletes_delete.spec.ts index 8a4bbe15..f29de5a0 100644 --- a/test/feature/query/deletes_delete.spec.ts +++ b/test/feature/query/deletes_delete.spec.ts @@ -9,7 +9,7 @@ describe('feature/query/deletes_delete', () => { @Str('') name!: string } - it('deletes a record specified by the where clause.', async () => { + it('deletes a record specified by the where clause', async () => { const store = createStore([User]) fillState(store, { @@ -33,7 +33,7 @@ describe('feature/query/deletes_delete', () => { }) }) - it('can delete multiple records specified by the where clause.', async () => { + it('can delete multiple records specified by the where clause', async () => { const store = createStore([User]) fillState(store, { From cf8ee0468f2ceeb31d83d06b8a0fc94040101085 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 22:11:01 +0900 Subject: [PATCH 35/56] feat: add `destroy` feature --- src/connection/Connection.ts | 13 +++- src/query/Query.ts | 63 ++++++++++++++++++- src/repository/Repository.ts | 14 +++++ test/feature/query/deletes_destroy.spec.ts | 72 ++++++++++++++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 test/feature/query/deletes_destroy.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index f3dfa0a1..841a0a4c 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -33,7 +33,7 @@ export default class Connection { } /** - * Find a record with the given id. + * Find a model by its primary key. */ find(id: string | number): Record | null { const namespace = this.store.$database.connection @@ -42,6 +42,17 @@ export default class Connection { return this.store.state[namespace][entity].data[id] ?? null } + /** + * Find multiple models by their primary keys. + */ + findIn(ids: string[]): Record[] { + const namespace = this.store.$database.connection + const entity = this.model.entity + const data = this.store.state[namespace][entity].data + + return ids.map((id) => data[id]) + } + /** * Commit the store mutation. */ diff --git a/src/query/Query.ts b/src/query/Query.ts index fd9f2b55..c6bba2d1 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -63,14 +63,27 @@ export default class Query { } /** - * Find the model with the given id. + * Find a model by its primary key. */ find(id: string | number): Item { - const record = this.connection().find(id) + const indexId = this.normalizeIndexId(id) + + const record = this.connection().find(indexId) return record ? this.hydrateRecord(record) : null } + /** + * Find multiple models by their primary keys. + */ + findIn(ids: (string | number)[]): Collection { + const indexIds = this.normalizeIndexIds(ids) + + const records = this.connection().findIn(indexIds) + + return this.hydrateRecords(records) + } + /** * Get all models from the state. The difference with the `get` is that this * method will not process any query chain. It'll always retrieve all models. @@ -167,6 +180,52 @@ export default class Query { return models } + /** + * Destroy the models for the given id. + */ + async destroy(id: string | number): Promise> { + const indexId = this.normalizeIndexId(id) + + const model = this.find(indexId) + + if (!model) { + return null + } + + this.connection().delete([indexId]) + + return model + } + + /** + * Destroy the models for the given id. + */ + async destroyMany(ids: (string | number)[]): Promise> { + const indexIds = this.normalizeIndexIds(ids) + + const models = this.findIn(indexIds) + + this.connection().delete(indexIds) + + return models + } + + /** + * Normalize the given index id. This method will convert the given key to + * the string since the index key must be a string. + */ + private normalizeIndexId(id: string | number): string { + return String(id) + } + + /** + * Normalize the given index ids. This method will convert the given key to + * the string since the index key must be a string. + */ + private normalizeIndexIds(ids: (string | number)[]): string[] { + return ids.map((id) => String(id)) + } + /** * Instantiate new models with the given record. */ diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index a0d7b4e4..36c5c369 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -162,4 +162,18 @@ export default class Repository { deleteAll(): Promise> { return this.query().deleteAll() } + + /** + * Destroy the models for the given id. + */ + destroy(id: string | number): Promise> { + return this.query().destroy(id) + } + + /** + * Destroy the models for the given ids. + */ + destroyMany(ids: (string | number)[]): Promise> { + return this.query().destroyMany(ids) + } } diff --git a/test/feature/query/deletes_destroy.spec.ts b/test/feature/query/deletes_destroy.spec.ts new file mode 100644 index 00000000..4ea373ce --- /dev/null +++ b/test/feature/query/deletes_destroy.spec.ts @@ -0,0 +1,72 @@ +import { createStore, fillState, assertState } from 'test/Helpers' +import { Model, Attr, Str } from '@/index' + +describe('feature/query/deletes_destroy', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + it('deletes record by the id', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + await store.$repo(User).destroy(2) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + }) + + it('returns `null` when no record was deleted', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + } + }) + + const user = await store.$repo(User).destroy(2) + + expect(user).toBe(null) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + } + }) + }) + + it('deletes multiple records by an array of ids', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + await store.$repo(User).destroyMany([2, 3]) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + } + }) + }) +}) From c41d230a70182afb2230648dd5e9ed7d145569c8 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Tue, 7 Apr 2020 23:40:59 +0900 Subject: [PATCH 36/56] feat: add `update` feature --- src/connection/Connection.ts | 7 +++ src/model/Model.ts | 31 ++++++----- src/model/attributes/Attribute.ts | 2 +- src/model/attributes/types/Attr.ts | 2 +- src/model/attributes/types/Boolean.ts | 2 +- src/model/attributes/types/Number.ts | 2 +- src/model/attributes/types/String.ts | 2 +- src/modules/Mutations.ts | 8 +++ src/query/Query.ts | 21 ++++++++ test/feature/query/updates_update.spec.ts | 63 +++++++++++++++++++++++ 10 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 test/feature/query/updates_update.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index 841a0a4c..081d92b3 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -70,6 +70,13 @@ export default class Connection { this.commit('insert', this.mapRecords(records)) } + /** + * Commit `update` change to the store. + */ + update(records: Record[]): void { + this.commit('update', this.mapRecords(records)) + } + /** * Convert the given array of records into records. */ diff --git a/src/model/Model.ts b/src/model/Model.ts index bd95143e..f0799c0a 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -97,7 +97,7 @@ export default class Model { /** * Get model fields for the model. */ - $fields(): Fields { + get $fields(): Fields { return this.$self.schema } @@ -106,16 +106,27 @@ export default class Model { * missing field. */ $fill(attributes: Record): this { - const fields = this.$fields() + for (const key in this.$fields) { + this.$fillField(key, attributes[key]) + } - for (const key in fields) { - const attr = fields[key] - const value = attributes[key] + return this + } + private $fillField(key: string, value: any): void { + const attr = this.$fields[key] + + if (value !== undefined) { this[key] = attr.make(value) + return } - return this + if (this[key] !== undefined) { + this[key] = this[key] + return + } + + this[key] = attr.make() } /** @@ -129,14 +140,10 @@ export default class Model { * Get the serialized model attributes. */ $getAttributes(): Record { - const fields = this.$fields() - const record = {} as Record - for (const key in fields) { - const value = this[key] - - record[key] = value + for (const key in this.$fields) { + record[key] = this[key] } return record diff --git a/src/model/attributes/Attribute.ts b/src/model/attributes/Attribute.ts index 6304e882..25752059 100644 --- a/src/model/attributes/Attribute.ts +++ b/src/model/attributes/Attribute.ts @@ -16,5 +16,5 @@ export default abstract class Attribute { /** * Make the value for the attribute. */ - abstract make(value: any): any + abstract make(value?: any): any } diff --git a/src/model/attributes/types/Attr.ts b/src/model/attributes/types/Attr.ts index 4e0d444f..8d56b705 100644 --- a/src/model/attributes/types/Attr.ts +++ b/src/model/attributes/types/Attr.ts @@ -5,7 +5,7 @@ export default class Attr extends Type { /** * Make the value for the attribute. */ - make(value: any): any { + make(value?: any): any { return value } } diff --git a/src/model/attributes/types/Boolean.ts b/src/model/attributes/types/Boolean.ts index bc89fd23..983dca2f 100644 --- a/src/model/attributes/types/Boolean.ts +++ b/src/model/attributes/types/Boolean.ts @@ -12,7 +12,7 @@ export default class Boolean extends Type { /** * Make the value for the attribute. */ - make(value: any): boolean | null { + make(value?: any): boolean | null { if (value === undefined) { return this.value } diff --git a/src/model/attributes/types/Number.ts b/src/model/attributes/types/Number.ts index 471a2fe9..c59b27a3 100644 --- a/src/model/attributes/types/Number.ts +++ b/src/model/attributes/types/Number.ts @@ -12,7 +12,7 @@ export default class Number extends Type { /** * Make the value for the attribute. */ - make(value: any): number | null { + make(value?: any): number | null { if (value === undefined) { return this.value } diff --git a/src/model/attributes/types/String.ts b/src/model/attributes/types/String.ts index a7e2f0d0..f50db826 100644 --- a/src/model/attributes/types/String.ts +++ b/src/model/attributes/types/String.ts @@ -12,7 +12,7 @@ export default class String extends Type { /** * Make the value for the attribute. */ - make(value: any): string | null { + make(value?: any): string | null { if (value === undefined) { return this.value } diff --git a/src/modules/Mutations.ts b/src/modules/Mutations.ts index 758f0463..79dd2700 100644 --- a/src/modules/Mutations.ts +++ b/src/modules/Mutations.ts @@ -15,6 +15,13 @@ function insert(state: State, records: Records): void { state.data = { ...state.data, ...records } } +/** + * Commit `update` change to the store. + */ +function update(state: State, records: Records): void { + state.data = { ...state.data, ...records } +} + /** * Commit `delete` change to the store. */ @@ -40,6 +47,7 @@ function destroyAll(state: State): void { export default { mutate, insert, + update, delete: destroy, deleteAll: destroyAll } diff --git a/src/query/Query.ts b/src/query/Query.ts index c6bba2d1..3e5cfcaf 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -158,6 +158,17 @@ export default class Query { return models } + /** + * Update records in the store. + */ + async update(record: Record): Promise> { + const models = this.mergeModelsWithRecord(this.get(), record) + + this.connection().update(this.dehydrateModels(models)) + + return models + } + /** * Delete records that match the query chain. */ @@ -247,6 +258,16 @@ export default class Query { return models.map((model) => model.$getAttributes()) } + /** + * Merge models with the given record. + */ + private mergeModelsWithRecord( + models: Collection, + record: Record + ): Collection { + return models.map((model) => model.$fill(record)) + } + /** * Get an array of ids from the given collection. */ diff --git a/test/feature/query/updates_update.spec.ts b/test/feature/query/updates_update.spec.ts new file mode 100644 index 00000000..7497af96 --- /dev/null +++ b/test/feature/query/updates_update.spec.ts @@ -0,0 +1,63 @@ +import { createStore, fillState, assertState } from 'test/Helpers' +import { Model, Attr, Str, Num } from '@/index' + +describe('feature/query/updates_update', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + @Num(0) age!: number + } + + it('updates a record specified by the query chain', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + await store + .$repo(User) + .where('name', 'Jane Doe') + .update({ age: 50 }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 50 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + }) + + it('updates multiple records specified by the query chain', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + await store + .$repo(User) + .where('name', 'Jane Doe') + .orWhere('age', 20) + .update({ age: 50 }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 50 }, + 3: { id: 3, name: 'Johnny Doe', age: 50 } + } + }) + }) +}) From 71393970f7d49bb654d65e660eb45b1bb896b3cc Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 00:06:40 +0900 Subject: [PATCH 37/56] chore: rename test case --- .../{updates_update.spec.ts => updates_update_query.spec.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/feature/query/{updates_update.spec.ts => updates_update_query.spec.ts} (96%) diff --git a/test/feature/query/updates_update.spec.ts b/test/feature/query/updates_update_query.spec.ts similarity index 96% rename from test/feature/query/updates_update.spec.ts rename to test/feature/query/updates_update_query.spec.ts index 7497af96..0dab56ce 100644 --- a/test/feature/query/updates_update.spec.ts +++ b/test/feature/query/updates_update_query.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState, assertState } from 'test/Helpers' import { Model, Attr, Str, Num } from '@/index' -describe('feature/query/updates_update', () => { +describe('feature/query/updates_update_query', () => { class User extends Model { static entity = 'users' From cf8974cdc19ed89acbf714fbef9c61e3af7116a8 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 01:00:33 +0900 Subject: [PATCH 38/56] feat: add normalizable update feature --- src/query/Query.ts | 56 ++++++++++++++++++--- src/repository/Repository.ts | 36 +++++++++++--- test/feature/query/updates_update.spec.ts | 59 +++++++++++++++++++++++ 3 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 test/feature/query/updates_update.spec.ts diff --git a/src/query/Query.ts b/src/query/Query.ts index 3e5cfcaf..f2cda857 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -66,9 +66,7 @@ export default class Query { * Find a model by its primary key. */ find(id: string | number): Item { - const indexId = this.normalizeIndexId(id) - - const record = this.connection().find(indexId) + const record = this.findRaw(id) return record ? this.hydrateRecord(record) : null } @@ -77,11 +75,25 @@ export default class Query { * Find multiple models by their primary keys. */ findIn(ids: (string | number)[]): Collection { - const indexIds = this.normalizeIndexIds(ids) + return this.hydrateRecords(this.findInRaw(ids)) + } + + /** + * Find a record by its primary key. + */ + findRaw(id: string | number): Record | null { + const indexId = this.normalizeIndexId(id) - const records = this.connection().findIn(indexIds) + return this.connection().find(indexId) + } + + /** + * Find multiple records by their primary keys. + */ + findInRaw(ids: (string | number)[]): Record[] { + const indexIds = this.normalizeIndexIds(ids) - return this.hydrateRecords(records) + return this.connection().findIn(indexIds) } /** @@ -158,6 +170,31 @@ export default class Query { return models } + /** + * Update records in the store by using the primary key of the given records. + */ + async merge(records: Record[]): Promise> { + const models = this.getMergedModels(records) + + this.connection().update(this.dehydrateModels(models)) + + return models + } + + /** + * Get models by merging the records. This method will use the primary key + * in the records to fetch models and merge the given record to the model. + */ + private getMergedModels(records: Record[]): Collection { + return records.reduce>((collection, record) => { + const model = this.find(this.model.getIndexId(record)) + + model && collection.push(this.mergeModelWithRecord(model, record)) + + return collection + }, []) + } + /** * Update records in the store. */ @@ -258,6 +295,13 @@ export default class Query { return models.map((model) => model.$getAttributes()) } + /** + * Merge the model with the given record. + */ + private mergeModelWithRecord(model: M, record: Record): M { + return model.$fill(record) + } + /** * Merge models with the given record. */ diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 36c5c369..0b30491f 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -5,6 +5,8 @@ import Constructor from '../model/Constructor' import Interpretation from '../interpretation/Interpretation' import Query from '../query/Query' +type PersistMethod = 'insert' | 'merge' + interface CollectionPromises { indexes: string[] promises: Promise>[] @@ -83,27 +85,49 @@ export default class Repository { /** * Insert the given record to the store. */ - async insert(record: Data.Record | Data.Record[]): Promise { + insert(record: Data.Record | Data.Record[]): Promise { + return this.persist('insert', record) + } + + /** + * Update records in the store. + */ + update(record: Data.Record | Data.Record[]): Promise { + return this.persist('merge', record) + } + + /** + * Persist records to the store by the given method. + */ + private persist( + method: PersistMethod, + record: Data.Record | Data.Record[] + ): Promise { const normalizedData = this.interpret(record) - const { indexes, promises } = this.createCollectionPromises(normalizedData) + const { indexes, promises } = this.createCollectionPromises( + method, + normalizedData + ) return this.resolveCollectionPromises(indexes, promises) } /** - * Insert the given normalized data. + * Persist normalized records with the given method. */ - private insertNormalizedData( + private persistRecords( + method: PersistMethod, records: Data.Records ): Promise> { - return this.query().insert(this.mapNormalizedData(records)) + return this.query()[method](this.mapNormalizedData(records)) } /** * Create collection promises for the given normalized data. */ private createCollectionPromises( + method: PersistMethod, data: Data.NormalizedData ): CollectionPromises { const indexes: string[] = [] @@ -114,7 +138,7 @@ export default class Repository { const repository = this.newRepository(entity) indexes.push(entity) - promises.push(repository.insertNormalizedData(records)) + promises.push(repository.persistRecords(method, records)) } return { indexes, promises } diff --git a/test/feature/query/updates_update.spec.ts b/test/feature/query/updates_update.spec.ts new file mode 100644 index 00000000..5332080f --- /dev/null +++ b/test/feature/query/updates_update.spec.ts @@ -0,0 +1,59 @@ +import { createStore, fillState, assertState } from 'test/Helpers' +import { Model, Attr, Str, Num } from '@/index' + +describe('feature/query/updates_update', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + @Num(0) age!: number + } + + it('updates record by passing record with primary key', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + await store.$repo(User).update({ id: 2, age: 50 }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 50 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + }) + + it('updates records by passing an array of records with primary key', async () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 } + } + }) + + await store.$repo(User).update([ + { id: 2, age: 50 }, + { id: 3, age: 60 } + ]) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 50 }, + 3: { id: 3, name: 'Johnny Doe', age: 60 } + } + }) + }) +}) From b3ff932385aa55f3c4153b56cb9ecdf5bc6d39f2 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 01:01:23 +0900 Subject: [PATCH 39/56] chore: organize method order a bit --- src/query/Query.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/query/Query.ts b/src/query/Query.ts index f2cda857..1e17d414 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -258,6 +258,13 @@ export default class Query { return models } + /** + * Get an array of ids from the given collection. + */ + private getIdsFromCollection(models: Collection): string[] { + return models.map((model) => model.$getIndexId()) + } + /** * Normalize the given index id. This method will convert the given key to * the string since the index key must be a string. @@ -311,11 +318,4 @@ export default class Query { ): Collection { return models.map((model) => model.$fill(record)) } - - /** - * Get an array of ids from the given collection. - */ - private getIdsFromCollection(models: Collection): string[] { - return models.map((model) => model.$getIndexId()) - } } From ba274decc7516f74a492f7e0aaf7892b42d73447 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 01:54:00 +0900 Subject: [PATCH 40/56] feat: add `orderBy` utility function --- src/support/Utils.ts | 111 +++++++++++++++++++++++ test/unit/support/Utils_Order_By.spec.ts | 99 ++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 test/unit/support/Utils_Order_By.spec.ts diff --git a/src/support/Utils.ts b/src/support/Utils.ts index 81c70b50..1fe2f5f0 100644 --- a/src/support/Utils.ts +++ b/src/support/Utils.ts @@ -1,3 +1,9 @@ +interface SortableArray { + criteria: any[] + index: number + value: T +} + export function isArray(value: any): value is any[] { return Array.isArray(value) } @@ -19,6 +25,111 @@ export function size(collection: any[] | object): number { : Object.keys(collection).length } +/** + * Creates an array of elements, sorted in specified order by the results + * of running each element in a collection thru each iteratee. + */ +export function orderBy( + collection: T[], + iteratees: (((record: T) => any) | string)[], + directions: string[] +): T[] { + let index = -1 + + const result = collection.map((value) => { + const criteria = iteratees.map((iteratee) => { + return typeof iteratee === 'function' ? iteratee(value) : value[iteratee] + }) + + return { criteria, index: ++index, value } + }) + + return baseSortBy(result, (object, other) => { + return compareMultiple(object, other, directions) + }) +} + +/** + * Creates an array of elements, sorted in ascending order by the results of + * running each element in a collection thru each iteratee. This method + * performs a stable sort, that is, it preserves the original sort order + * of equal elements. + */ +function baseSortBy( + array: SortableArray[], + comparer: (a: SortableArray, B: SortableArray) => number +): T[] { + let length = array.length + + array.sort(comparer) + + const newArray: T[] = [] + while (length--) { + newArray[length] = array[length].value + } + return newArray +} + +/** + * Used by `orderBy` to compare multiple properties of a value to another + * and stable sort them. + * + * If `orders` is unspecified, all values are sorted in ascending order. + * Otherwise, specify an order of "desc" for descending or "asc" for + * ascending sort order of corresponding values. + */ +function compareMultiple(object: any, other: any, orders: string[]): number { + let index = -1 + + const objCriteria = object.criteria + const othCriteria = other.criteria + const length = objCriteria.length + + while (++index < length) { + const result = compareAscending(objCriteria[index], othCriteria[index]) + + if (result) { + const order = orders[index] + + return result * (order === 'desc' ? -1 : 1) + } + } + + return object.index - other.index +} + +/** + * Compares values to sort them in ascending order. + */ +function compareAscending(value: any, other: any): number { + if (value !== other) { + const valIsDefined = value !== undefined + const valIsNull = value === null + const valIsReflexive = value === value + + const othIsDefined = other !== undefined + const othIsNull = other === null + + if (typeof value !== 'number' || typeof other !== 'number') { + value = String(value) + other = String(other) + } + + if ( + (!othIsNull && value > other) || + (valIsNull && othIsDefined) || + !valIsDefined || + !valIsReflexive + ) { + return 1 + } + + return -1 + } + + return 0 +} + /** * Creates an object composed of keys generated from the results of running * each element of collection through iteratee. diff --git a/test/unit/support/Utils_Order_By.spec.ts b/test/unit/support/Utils_Order_By.spec.ts new file mode 100644 index 00000000..bbd1d25e --- /dev/null +++ b/test/unit/support/Utils_Order_By.spec.ts @@ -0,0 +1,99 @@ +import { orderBy } from '@/support/Utils' + +describe('unit/support/Utils_Order_by', () => { + it('can order collection by given key in asc order', () => { + const collection = [{ id: 2 }, { id: 3 }, { id: 10 }, { id: 1 }] + + const expected = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 10 }] + + expect(orderBy(collection, ['id'], ['asc'])).toEqual(expected) + }) + + it('can order collection by given key in desc order', () => { + const collection = [{ id: 2 }, { id: 3 }, { id: 10 }, { id: 1 }] + + const expected = [{ id: 10 }, { id: 3 }, { id: 2 }, { id: 1 }] + + expect(orderBy(collection, ['id'], ['desc'])).toEqual(expected) + }) + + it('can order collection of mixed value types', () => { + const collection = [ + { id: 2 }, + { id: 'id3' }, + { name: 'John Doe' }, + { name: 'Andy Newman' }, + { id: null }, + { id: 'id1' }, + { id: 1 } + ] + + const expected = [ + { id: 1 }, + { id: 2 }, + { id: 'id1' }, + { id: 'id3' }, + { id: null }, + { name: 'John Doe' }, + { name: 'Andy Newman' } + ] + + expect(orderBy(collection, ['id'], ['asc'])).toEqual(expected) + }) + + it('can order collection with multiple keys', () => { + const collection = [ + { id: 1, name: 'John Doe' }, + { id: 3, name: 'Peter Ericsson' }, + { id: 2, name: 'Andrew Black' }, + { id: 3, name: 'George Mac' }, + { id: 1, name: 'Bob Green' }, + { id: 2, name: 'Chris Brian' } + ] + + const expected = [ + { id: 1, name: 'Bob Green' }, + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Andrew Black' }, + { id: 2, name: 'Chris Brian' }, + { id: 3, name: 'George Mac' }, + { id: 3, name: 'Peter Ericsson' } + ] + + expect(orderBy(collection, ['id', 'name'], ['asc', 'asc'])).toEqual( + expected + ) + }) + + it('can order collection with multiple keys with mixed key types', () => { + const collection = [ + { id: 1, name: 'John Doe' }, + { name: 'Peter Ericsson' }, + { id: 2, name: 'Andrew Black' }, + { name: 'George Mac' }, + { id: 1 }, + { id: 2, name: 'Chris Brian' } + ] + + const expected = [ + { id: 1 }, + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Chris Brian' }, + { id: 2, name: 'Andrew Black' }, + { name: 'Peter Ericsson' }, + { name: 'George Mac' } + ] + + expect(orderBy(collection, ['id', 'name'], ['asc', 'desc'])).toEqual( + expected + ) + }) + + it('can order collection with function', () => { + const collection = [{ id: 2 }, { id: 3 }, { id: 1 }] + + const expected = [{ id: 1 }, { id: 2 }, { id: 3 }] + + expect(orderBy(collection, [(v) => v.id], ['asc'])).toEqual(expected) + }) +}) From fb1444f9880117aee8db09eb7e4046c048c2932a Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 01:58:01 +0900 Subject: [PATCH 41/56] feat: add `cloneDeep` utility function --- src/support/Utils.ts | 24 ++++++++++++++++++++ test/unit/support/Utils_Clone_Deep.spec.ts | 26 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 test/unit/support/Utils_Clone_Deep.spec.ts diff --git a/src/support/Utils.ts b/src/support/Utils.ts index 1fe2f5f0..0eb430f6 100644 --- a/src/support/Utils.ts +++ b/src/support/Utils.ts @@ -150,3 +150,27 @@ export function groupBy( return records }, {}) } + +/** + * Deep clone the given target object. + */ +export function cloneDeep(target: T): T { + if (isArray(target)) { + const cp = [] as any[] + ;(target as any[]).forEach((v) => cp.push(v)) + + return cp.map((n: any) => cloneDeep(n)) as any + } + + if (typeof target === 'object' && target !== {}) { + const cp = { ...(target as { [key: string]: any }) } as { + [key: string]: any + } + + Object.keys(cp).forEach((k) => (cp[k] = cloneDeep(cp[k]))) + + return cp as T + } + + return target +} diff --git a/test/unit/support/Utils_Clone_Deep.spec.ts b/test/unit/support/Utils_Clone_Deep.spec.ts new file mode 100644 index 00000000..5e844a92 --- /dev/null +++ b/test/unit/support/Utils_Clone_Deep.spec.ts @@ -0,0 +1,26 @@ +import { cloneDeep } from '@/support/Utils' + +describe('unit/support/Utils_Clone_Deep', () => { + it('can create a deep clone of an object', () => { + const data = { + id: 1, + nested: [ + { + id: 1, + deep: [ + { id: 1, deeper: { id: 2 } }, + { id: 2, deeper: { id: 3 } } + ] + } + ] + } + + const clone = cloneDeep(data) + + expect(clone).toStrictEqual(data) + + delete data.nested[0].deep[0].deeper.id + + expect(clone).not.toStrictEqual(data) + }) +}) From 4f3d5ac9a5199a76d5557fafa3818075265edf14 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 02:56:42 +0900 Subject: [PATCH 42/56] refactor: organize exports --- package.json | 2 +- src/database/Database.ts | 2 +- src/index.cjs.ts | 20 +++++++++++++++++++- src/index.ts | 35 +++++++++++++++++++++++++++++------ src/modules/Module.ts | 2 +- src/modules/Mutations.ts | 5 +++++ src/polyfills/Polyfills.ts | 1 + yarn.lock | 7 ++++++- 8 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 src/polyfills/Polyfills.ts diff --git a/package.json b/package.json index 2f80bb53..9d5b98d8 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "chalk": "^3.0.0", "codecov": "^3.6.4", "conventional-changelog-cli": "^2.0.31", - "core-js": "^2.6.11", + "core-js": "^3.6.4", "enquirer": "^2.3.4", "execa": "^4.0.0", "jest": "^25.2.4", diff --git a/src/database/Database.ts b/src/database/Database.ts index 719f7837..65b2d49c 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -145,7 +145,7 @@ export default class Database { /** * Create sub module. */ - private createSubModule(): Module { + private createSubModule(): Module { return { namespaced: true, state: this.createSubState(), diff --git a/src/index.cjs.ts b/src/index.cjs.ts index 91f17404..145776bb 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,9 +1,27 @@ +import './polyfills/Polyfills' + import { install } from './store/Store' import Database from './database/Database' +import Schema from './schema/Schema' import Model from './model/Model' +import * as Attributes from './model/attributes/Attributes' +import Mutations from './modules/Mutations' +import Repository from './repository/Repository' +import Interpretation from './interpretation/Interpretation' +import Query from './query/Query' +import * as QueryOptions from './query/options/Options' +import Connection from './connection/Connection' export default { install, Database, - Model + Schema, + Model, + Attributes, + Mutations, + Repository, + Interpretation, + Query, + QueryOptions, + Connection } diff --git a/src/index.ts b/src/index.ts index 49779941..2ba178e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,12 @@ +import './polyfills/Polyfills' + export * from './data/Data' import { install, Options } from './store/Store' import Database from './database/Database' -import Model from './model/Model' +import Schema from './schema/Schema' +import Model, { Fields } from './model/Model' +import * as Attributes from './model/attributes/Attributes' import Attr from './model/decorators/types/Attr' import Str from './model/decorators/types/Str' import Num from './model/decorators/types/Num' @@ -11,12 +15,21 @@ import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' import State from './modules/State' +import Mutations from './modules/Mutations' +import Repository from './repository/Repository' +import Interpretation from './interpretation/Interpretation' +import Query from './query/Query' +import * as QueryOptions from './query/options/Options' +import Connection from './connection/Connection' export { install, Options, Database, + Schema, Model, + Fields, + Attributes, Attr, Str, Num, @@ -24,15 +37,25 @@ export { RootModule, RootState, Module, - State + State, + Mutations, + Repository, + Interpretation, + Query, + QueryOptions, + Connection } export default { install, Database, + Schema, Model, - Attr, - Str, - Num, - Bool + Attributes, + Mutations, + Repository, + Interpretation, + Query, + QueryOptions, + Connection } diff --git a/src/modules/Module.ts b/src/modules/Module.ts index 5c4153aa..fdbad937 100644 --- a/src/modules/Module.ts +++ b/src/modules/Module.ts @@ -1,7 +1,7 @@ import { Module as BaseModule } from 'vuex' import State from './State' -export default interface Module extends BaseModule { +export default interface Module extends BaseModule { namespaced: true state: S } diff --git a/src/modules/Mutations.ts b/src/modules/Mutations.ts index 79dd2700..f8582e69 100644 --- a/src/modules/Mutations.ts +++ b/src/modules/Mutations.ts @@ -1,6 +1,11 @@ +import { MutationTree } from 'vuex' import { Records } from '../data/Data' import State from './State' +export interface Mutations extends MutationTree { + mutate(state: State, callback: (state: S) => void): void +} + /** * Generic mutation. */ diff --git a/src/polyfills/Polyfills.ts b/src/polyfills/Polyfills.ts new file mode 100644 index 00000000..02b7ba52 --- /dev/null +++ b/src/polyfills/Polyfills.ts @@ -0,0 +1 @@ +import 'core-js/es/array/includes' diff --git a/yarn.lock b/yarn.lock index 09e88d72..8f6683d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2918,11 +2918,16 @@ copy-webpack-plugin@^5.0.2: serialize-javascript "^2.1.2" webpack-log "^2.0.0" -core-js@^2.6.11, core-js@^2.6.5: +core-js@^2.6.5: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== +core-js@^3.6.4: + version "3.6.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" + integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" From 335bf0e9921cc58ecb274363640f4f54dae7bd58 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 15:44:25 +0900 Subject: [PATCH 43/56] refactor: rethink how model instance get handled by components --- src/connection/Connection.ts | 64 +++++++++------ src/database/Database.ts | 25 ++++-- src/index.ts | 8 +- src/interpretation/Interpretation.ts | 7 +- src/model/Constructor.ts | 21 ----- src/model/Model.ts | 81 ++++++++++++++----- src/model/attributes/Attributes.ts | 4 +- src/model/attributes/relations/HasOne.ts | 33 ++++++++ src/model/attributes/relations/Relation.ts | 26 ++++++ src/model/attributes/types/Attr.ts | 2 +- src/model/attributes/types/Boolean.ts | 4 +- src/model/attributes/types/Number.ts | 4 +- src/model/attributes/types/String.ts | 4 +- src/model/attributes/types/Type.ts | 6 +- src/model/decorators/Contracts.ts | 7 ++ src/model/decorators/attributes/types/Attr.ts | 10 +++ src/model/decorators/attributes/types/Bool.ts | 19 +++++ src/model/decorators/attributes/types/Num.ts | 19 +++++ src/model/decorators/attributes/types/Str.ts | 19 +++++ .../decorators/contracts/PropertyDecorator.ts | 5 -- src/model/decorators/types/Attr.ts | 13 --- src/model/decorators/types/Bool.ts | 26 ------ src/model/decorators/types/Num.ts | 26 ------ src/model/decorators/types/Str.ts | 26 ------ src/query/Query.ts | 9 +-- src/repository/Repository.ts | 8 +- src/schema/Schema.ts | 9 +-- src/store/Store.ts | 2 +- src/types/index.ts | 1 + src/types/vuex.d.ts | 2 +- test/feature/relations/has_one_insert.spec.ts | 44 ++++++++++ test/unit/model/Model_Attrs_Boolean.spec.ts | 2 +- test/unit/model/Model_Attrs_Number.spec.ts | 2 +- test/unit/model/Model_Attrs_String.spec.ts | 2 +- 34 files changed, 334 insertions(+), 206 deletions(-) delete mode 100644 src/model/Constructor.ts create mode 100644 src/model/attributes/relations/HasOne.ts create mode 100644 src/model/attributes/relations/Relation.ts create mode 100644 src/model/decorators/Contracts.ts create mode 100644 src/model/decorators/attributes/types/Attr.ts create mode 100644 src/model/decorators/attributes/types/Bool.ts create mode 100644 src/model/decorators/attributes/types/Num.ts create mode 100644 src/model/decorators/attributes/types/Str.ts delete mode 100644 src/model/decorators/contracts/PropertyDecorator.ts delete mode 100644 src/model/decorators/types/Attr.ts delete mode 100644 src/model/decorators/types/Bool.ts delete mode 100644 src/model/decorators/types/Num.ts delete mode 100644 src/model/decorators/types/Str.ts create mode 100644 src/types/index.ts create mode 100644 test/feature/relations/has_one_insert.spec.ts diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index 081d92b3..4eaf2def 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -1,7 +1,11 @@ import { Store } from 'vuex' import { Record, Records } from '../data/Data' import Model from '../model/Model' -import Constructor from '../model/Constructor' + +interface Namespace { + connection: string + entity: string +} export default class Connection { /** @@ -12,55 +16,65 @@ export default class Connection { /** * The model object. */ - model: Constructor + model: M /** * Create a new connection instance. */ - constructor(store: Store, model: Constructor) { + constructor(store: Store, model: M) { this.store = store this.model = model } /** - * Get all existing records. + * Get namespace for the modul */ - get(): Records { - const namespace = this.store.$database.connection - const entity = this.model.entity + private getNamespace(): Namespace { + const connection = this.store.$database.connection + const entity = this.model.$entity - return this.store.state[namespace][entity].data + return { connection, entity } } /** - * Find a model by its primary key. + * Get the state object from the store. */ - find(id: string | number): Record | null { - const namespace = this.store.$database.connection - const entity = this.model.entity + private getData(): Records { + const { connection, entity } = this.getNamespace() - return this.store.state[namespace][entity].data[id] ?? null + return this.store.state[connection][entity].data } /** - * Find multiple models by their primary keys. + * Commit the store mutation. */ - findIn(ids: string[]): Record[] { - const namespace = this.store.$database.connection - const entity = this.model.entity - const data = this.store.state[namespace][entity].data + private commit(name: string, payload?: any): void { + const { connection, entity } = this.getNamespace() - return ids.map((id) => data[id]) + this.store.commit(`${connection}/${entity}/${name}`, payload) } /** - * Commit the store mutation. + * Get all existing records. */ - private commit(name: string, payload?: any): void { - const namespace = this.store.$database.connection - const entity = this.model.entity + get(): Records { + return this.getData() + } + + /** + * Find a model by its primary key. + */ + find(id: string | number): Record | null { + return this.getData()[id] ?? null + } - this.store.commit(`${namespace}/${entity}/${name}`, payload) + /** + * Find multiple models by their primary keys. + */ + findIn(ids: string[]): Record[] { + const data = this.getData() + + return ids.map((id) => data[id]) } /** @@ -82,7 +96,7 @@ export default class Connection { */ private mapRecords(records: Record[]): Records { return records.reduce((carry, record) => { - carry[this.model.getIndexId(record)] = record + carry[this.model.$getIndexId(record)] = record return carry }, {}) } diff --git a/src/database/Database.ts b/src/database/Database.ts index 65b2d49c..ff8a1504 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -1,15 +1,15 @@ import { Store, Module as VuexModule, MutationTree } from 'vuex' import { schema as Normalizr } from 'normalizr' +import { Constructor } from '../types' import Schema from '../schema/Schema' import Model from '../model/Model' -import Constructor from '../model/Constructor' import RootModule from '../modules/RootModule' import Module from '../modules/Module' import State from '../modules/State' import Mutations from '../modules/Mutations' interface Models { - [name: string]: Constructor + [name: string]: Model } interface Schemas { @@ -48,7 +48,9 @@ export default class Database { * Register the given model. */ register(model: Constructor): void { - this.models[model.entity] = model + const instance = new model() + + this.models[instance.$entity] = instance } /** @@ -73,6 +75,8 @@ export default class Database { * Initialize the database before a user can start using it. */ start(): void { + this.injectStoreToModels() + this.createSchemas() this.registerModules() @@ -83,8 +87,8 @@ export default class Database { /** * Get the model. */ - getModel(name: string): Constructor { - return this.models[name] as Constructor + getModel(name: string): M { + return this.models[name] as M } /** @@ -94,6 +98,15 @@ export default class Database { return this.schemas[name] } + /** + * Inject the store instance to all registered models. + */ + private injectStoreToModels(): void { + for (const name in this.models) { + this.models[name].$setStore(this.store) + } + } + /** * Create the schema definition from registered models and set it to the * `schema` property. This schema will be used by the interpretation @@ -108,7 +121,7 @@ export default class Database { /** * Create schema from the given model. */ - private createSchema(model: Constructor): Normalizr.Entity { + private createSchema(model: M): Normalizr.Entity { return new Schema(model).one() } diff --git a/src/index.ts b/src/index.ts index 2ba178e4..47564dfa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,10 +7,10 @@ import Database from './database/Database' import Schema from './schema/Schema' import Model, { Fields } from './model/Model' import * as Attributes from './model/attributes/Attributes' -import Attr from './model/decorators/types/Attr' -import Str from './model/decorators/types/Str' -import Num from './model/decorators/types/Num' -import Bool from './model/decorators/types/Bool' +import Attr from './model/decorators/attributes/types/Attr' +import Str from './model/decorators/attributes/types/Str' +import Num from './model/decorators/attributes/types/Num' +import Bool from './model/decorators/attributes/types/Bool' import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' diff --git a/src/interpretation/Interpretation.ts b/src/interpretation/Interpretation.ts index 466233c2..2577e862 100644 --- a/src/interpretation/Interpretation.ts +++ b/src/interpretation/Interpretation.ts @@ -3,7 +3,6 @@ import { Store } from 'vuex' import { isArray, isEmpty } from '../support/Utils' import { Record, NormalizedData } from '../data/Data' import Model from '../model/Model' -import Constructor from '../model/Constructor' export default class Interpretation { /** @@ -14,12 +13,12 @@ export default class Interpretation { /** * The model object. */ - model: Constructor + model: M /** * Create a new interpreter instance. */ - constructor(store: Store, model: Constructor) { + constructor(store: Store, model: M) { this.store = store this.model = model } @@ -42,6 +41,6 @@ export default class Interpretation { * Get the schema from the database. */ private getSchema(): Normalizr.Entity { - return this.store.$database.getSchema(this.model.entity) + return this.store.$database.getSchema(this.model.$entity) } } diff --git a/src/model/Constructor.ts b/src/model/Constructor.ts deleted file mode 100644 index 590fa84e..00000000 --- a/src/model/Constructor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Record } from '../data/Data' -import * as Attributes from './attributes/Attributes' -import Model from './Model' - -export default interface Constructor { - entity: string - - primaryKey: string - - new (...args: any[]): M - - setSchema( - this: M, - key: string, - attribute: Attributes.Attribute - ): M - - attr(this: M): Attributes.Attribute - - getIndexId(record: Record): string -} diff --git a/src/model/Model.ts b/src/model/Model.ts index f0799c0a..9c73c990 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,3 +1,4 @@ +import { Store } from 'vuex' import { Record } from '../data/Data' import * as Attributes from './attributes/Attributes' @@ -6,6 +7,11 @@ export interface Fields { } export default class Model { + /** + * The store instance. + */ + protected _store!: Store + /** * The name of the model. */ @@ -25,12 +31,12 @@ export default class Model { /** * Create a new model instance. */ - constructor(record: Record) { - this.$fill(record) + constructor(attributes?: Record) { + this.$fill(attributes) } /** - * Set the attribute. + * Set the attribute to the schema. */ static setSchema( this: M, @@ -43,14 +49,14 @@ export default class Model { } /** - * Create a new attr attribute class. + * Create a new attr attribute instance. */ - static attr(this: M): Attributes.Attr { - return new Attributes.Attr(this) + static attr(this: M, value: any): Attributes.Attr { + return new Attributes.Attr(this, value) } /** - * Create a new string attribute class. + * Create a new string attribute instance. */ static string( this: M, @@ -60,7 +66,7 @@ export default class Model { } /** - * Create a new number attribute class. + * Create a new number attribute instance. */ static number( this: M, @@ -70,7 +76,7 @@ export default class Model { } /** - * Create a new boolean attribute class. + * Create a new boolean attribute instance. */ static boolean( this: M, @@ -80,18 +86,51 @@ export default class Model { } /** - * Get the index id from the given record. An index ID is a value that - * used as a index key for records within the store. + * Get the constructor for the model. */ - static getIndexId(record: Record): string { - return String(record[this.primaryKey]) + get $self(): typeof Model { + return this.constructor as typeof Model } /** - * Get the constructor for the model. + * Get the store instance. */ - get $self(): typeof Model { - return this.constructor as typeof Model + get $store(): Store { + return this._store + } + + /** + * Get the entity for the model. + */ + get $entity(): string { + return this.$self.entity + } + + /** + * Get the primary key for the model. + */ + get $primaryKey(): string { + return this.$self.primaryKey + } + + /** + * Set the store instance. + */ + $setStore(store: Store): this { + this._store = store + + return this + } + + /** + * Create a new instance of the model. This method just provides a convenient + * way for us to generate fresh model instances of this current model. It's + * particularly useful during the hydration of new objects via the query. + */ + $newInstance(attributes: Record): this { + const model = this.$self as any + + return new model(attributes) } /** @@ -105,7 +144,7 @@ export default class Model { * Fill the model by the given attributes. Its default values will fill any * missing field. */ - $fill(attributes: Record): this { + $fill(attributes: Record = {}): this { for (const key in this.$fields) { this.$fillField(key, attributes[key]) } @@ -130,10 +169,12 @@ export default class Model { } /** - * Get the index id value for the model. + * Get the index id for the model or the given record. */ - $getIndexId(): string { - return this.$self.getIndexId(this) + $getIndexId(record?: Record): string { + const target = record ?? this + + return String(target[this.$primaryKey]) } /** diff --git a/src/model/attributes/Attributes.ts b/src/model/attributes/Attributes.ts index a203cc57..2393489c 100644 --- a/src/model/attributes/Attributes.ts +++ b/src/model/attributes/Attributes.ts @@ -4,5 +4,7 @@ import Attr from './types/Attr' import String from './types/String' import Number from './types/Number' import Boolean from './types/Boolean' +import Relation from './relations/Relation' +import HasOne from './relations/HasOne' -export { Attribute, Type, Attr, String, Number, Boolean } +export { Attribute, Type, Attr, String, Number, Boolean, Relation, HasOne } diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts new file mode 100644 index 00000000..eb63f2f2 --- /dev/null +++ b/src/model/attributes/relations/HasOne.ts @@ -0,0 +1,33 @@ +import Model from '../../Model' +import Relation from './Relation' + +export default class String< + P extends typeof Model, + R extends typeof Model +> extends Relation { + /** + * The foreign key of the parent model. + */ + protected foreignKey: string + + /** + * The local key of the parent model. + */ + protected localKey: string + + /** + * Create a new has one relation instance. + */ + constructor(parent: P, related: R, foreignKey: string, localKey: string) { + super(parent, related) + this.foreignKey = foreignKey + this.localKey = localKey + } + + /** + * Make the value for the attribute. + */ + make(_value: any): null { + return null + } +} diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts new file mode 100644 index 00000000..0adb899e --- /dev/null +++ b/src/model/attributes/relations/Relation.ts @@ -0,0 +1,26 @@ +import Model from '../../Model' +import Attribute from '../Attribute' + +export default abstract class Relelation< + P extends typeof Model, + R extends typeof Model +> extends Attribute

{ + /** + * The parent model. + */ + protected parent: P + + /** + * The related model. + */ + protected related: R + + /** + * Create a new relation instance. + */ + constructor(parent: P, related: R) { + super(parent) + this.parent = parent + this.related = related + } +} diff --git a/src/model/attributes/types/Attr.ts b/src/model/attributes/types/Attr.ts index 8d56b705..4e0d444f 100644 --- a/src/model/attributes/types/Attr.ts +++ b/src/model/attributes/types/Attr.ts @@ -5,7 +5,7 @@ export default class Attr extends Type { /** * Make the value for the attribute. */ - make(value?: any): any { + make(value: any): any { return value } } diff --git a/src/model/attributes/types/Boolean.ts b/src/model/attributes/types/Boolean.ts index 983dca2f..8b639a87 100644 --- a/src/model/attributes/types/Boolean.ts +++ b/src/model/attributes/types/Boolean.ts @@ -3,7 +3,7 @@ import Type from './Type' export default class Boolean extends Type { /** - * Create a new boolean attribute. + * Create a new boolean attribute instance. */ constructor(model: M, value: boolean | null) { super(model, value) @@ -12,7 +12,7 @@ export default class Boolean extends Type { /** * Make the value for the attribute. */ - make(value?: any): boolean | null { + make(value: any): boolean | null { if (value === undefined) { return this.value } diff --git a/src/model/attributes/types/Number.ts b/src/model/attributes/types/Number.ts index c59b27a3..c9677cee 100644 --- a/src/model/attributes/types/Number.ts +++ b/src/model/attributes/types/Number.ts @@ -3,7 +3,7 @@ import Type from './Type' export default class Number extends Type { /** - * Create a new number attribute. + * Create a new number attribute instance. */ constructor(model: M, value: number | null) { super(model, value) @@ -12,7 +12,7 @@ export default class Number extends Type { /** * Make the value for the attribute. */ - make(value?: any): number | null { + make(value: any): number | null { if (value === undefined) { return this.value } diff --git a/src/model/attributes/types/String.ts b/src/model/attributes/types/String.ts index f50db826..61472b61 100644 --- a/src/model/attributes/types/String.ts +++ b/src/model/attributes/types/String.ts @@ -3,7 +3,7 @@ import Type from './Type' export default class String extends Type { /** - * Create a new string attribute. + * Create a new string attribute instance. */ constructor(model: M, value: string | null) { super(model, value) @@ -12,7 +12,7 @@ export default class String extends Type { /** * Make the value for the attribute. */ - make(value?: any): string | null { + make(value: any): string | null { if (value === undefined) { return this.value } diff --git a/src/model/attributes/types/Type.ts b/src/model/attributes/types/Type.ts index 03f0d76a..558e8e18 100644 --- a/src/model/attributes/types/Type.ts +++ b/src/model/attributes/types/Type.ts @@ -7,15 +7,15 @@ export default abstract class Type extends Attribute< /** * The default value for the attribute. */ - value: any + protected value: any /** * Whether if the attribute accepts `null` value or not. */ - isNullable: boolean = false + protected isNullable: boolean = false /** - * Create a new string attribute. + * Create a new type attribute instance. */ constructor(model: M, value: any = null) { super(model) diff --git a/src/model/decorators/Contracts.ts b/src/model/decorators/Contracts.ts new file mode 100644 index 00000000..88fb5672 --- /dev/null +++ b/src/model/decorators/Contracts.ts @@ -0,0 +1,7 @@ +import Model from '../Model' + +export type PropertyDecorator = (target: Model, propertyKey: string) => void + +export interface TypeOptions { + nullable?: boolean +} diff --git a/src/model/decorators/attributes/types/Attr.ts b/src/model/decorators/attributes/types/Attr.ts new file mode 100644 index 00000000..2d207679 --- /dev/null +++ b/src/model/decorators/attributes/types/Attr.ts @@ -0,0 +1,10 @@ +import { PropertyDecorator } from '../../Contracts' + +/** + * Create an attr decorator. + */ +export default function Attr(value?: any): PropertyDecorator { + return (target, propertyKey) => { + target.$self.setSchema(propertyKey, target.$self.attr(value)) + } +} diff --git a/src/model/decorators/attributes/types/Bool.ts b/src/model/decorators/attributes/types/Bool.ts new file mode 100644 index 00000000..2cc8191a --- /dev/null +++ b/src/model/decorators/attributes/types/Bool.ts @@ -0,0 +1,19 @@ +import { PropertyDecorator, TypeOptions } from '../../Contracts' + +/** + * Create a str decorator. + */ +export default function Str( + value: boolean | null, + options: TypeOptions = {} +): PropertyDecorator { + return (target, propertyKey) => { + const attr = target.$self.boolean(value) + + if (options.nullable) { + attr.nullable() + } + + target.$self.setSchema(propertyKey, attr) + } +} diff --git a/src/model/decorators/attributes/types/Num.ts b/src/model/decorators/attributes/types/Num.ts new file mode 100644 index 00000000..d0da0ab2 --- /dev/null +++ b/src/model/decorators/attributes/types/Num.ts @@ -0,0 +1,19 @@ +import { PropertyDecorator, TypeOptions } from '../../Contracts' + +/** + * Create a str decorator. + */ +export default function Str( + value: number | null, + options: TypeOptions = {} +): PropertyDecorator { + return (target, propertyKey) => { + const attr = target.$self.number(value) + + if (options.nullable) { + attr.nullable() + } + + target.$self.setSchema(propertyKey, attr) + } +} diff --git a/src/model/decorators/attributes/types/Str.ts b/src/model/decorators/attributes/types/Str.ts new file mode 100644 index 00000000..b35711a0 --- /dev/null +++ b/src/model/decorators/attributes/types/Str.ts @@ -0,0 +1,19 @@ +import { PropertyDecorator, TypeOptions } from '../../Contracts' + +/** + * Create a str decorator. + */ +export default function Str( + value: string | null, + options: TypeOptions = {} +): PropertyDecorator { + return (target, propertyKey) => { + const attr = target.$self.string(value) + + if (options.nullable) { + attr.nullable() + } + + target.$self.setSchema(propertyKey, attr) + } +} diff --git a/src/model/decorators/contracts/PropertyDecorator.ts b/src/model/decorators/contracts/PropertyDecorator.ts deleted file mode 100644 index bc138ed9..00000000 --- a/src/model/decorators/contracts/PropertyDecorator.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Model from '../../Model' - -type PropertyDecorator = (target: Model, propertyKey: string) => void - -export default PropertyDecorator diff --git a/src/model/decorators/types/Attr.ts b/src/model/decorators/types/Attr.ts deleted file mode 100644 index 954396f9..00000000 --- a/src/model/decorators/types/Attr.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Model from '../../Model' -import PropertyDecorator from '../contracts/PropertyDecorator' - -/** - * Create an attr decorator. - */ -export default function Attr(): PropertyDecorator { - return (target, propertyKey) => { - const model = target.constructor as typeof Model - - model.setSchema(propertyKey, model.attr()) - } -} diff --git a/src/model/decorators/types/Bool.ts b/src/model/decorators/types/Bool.ts deleted file mode 100644 index 4645b8f2..00000000 --- a/src/model/decorators/types/Bool.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Model from '../../Model' -import PropertyDecorator from '../contracts/PropertyDecorator' - -interface Options { - nullable?: boolean -} - -/** - * Create a bool decorator. - */ -export default function Bool( - value: boolean | null, - options: Options = {} -): PropertyDecorator { - return (target, propertyKey) => { - const model = target.constructor as typeof Model - - const attr = model.boolean(value) - - if (options.nullable) { - attr.nullable() - } - - model.setSchema(propertyKey, attr) - } -} diff --git a/src/model/decorators/types/Num.ts b/src/model/decorators/types/Num.ts deleted file mode 100644 index bddc9cd4..00000000 --- a/src/model/decorators/types/Num.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Model from '../../Model' -import PropertyDecorator from '../contracts/PropertyDecorator' - -interface Options { - nullable?: boolean -} - -/** - * Create a num decorator. - */ -export default function Num( - value: number | null, - options: Options = {} -): PropertyDecorator { - return (target, propertyKey) => { - const model = target.constructor as typeof Model - - const attr = model.number(value) - - if (options.nullable) { - attr.nullable() - } - - model.setSchema(propertyKey, attr) - } -} diff --git a/src/model/decorators/types/Str.ts b/src/model/decorators/types/Str.ts deleted file mode 100644 index f9da3c94..00000000 --- a/src/model/decorators/types/Str.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Model from '../../Model' -import PropertyDecorator from '../contracts/PropertyDecorator' - -interface Options { - nullable?: boolean -} - -/** - * Create a str decorator. - */ -export default function Str( - value: string | null, - options: Options = {} -): PropertyDecorator { - return (target, propertyKey) => { - const model = target.constructor as typeof Model - - const attr = model.string(value) - - if (options.nullable) { - attr.nullable() - } - - model.setSchema(propertyKey, attr) - } -} diff --git a/src/query/Query.ts b/src/query/Query.ts index 1e17d414..e5c8bc0e 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -2,7 +2,6 @@ import { Store } from 'vuex' import { isEmpty, groupBy } from '../support/Utils' import { Record, Item, Collection } from '../data/Data' import Model from '../model/Model' -import Constructor from '../model/Constructor' import Connection from '../connection/Connection' import * as Options from './options/Options' @@ -15,7 +14,7 @@ export default class Query { /** * The model object. */ - model: Constructor + model: M /** * The where constraints for the query. @@ -25,7 +24,7 @@ export default class Query { /** * Create a new query instance. */ - constructor(store: Store, model: Constructor) { + constructor(store: Store, model: M) { this.store = store this.model = model } @@ -187,7 +186,7 @@ export default class Query { */ private getMergedModels(records: Record[]): Collection { return records.reduce>((collection, record) => { - const model = this.find(this.model.getIndexId(record)) + const model = this.find(this.model.$getIndexId(record)) model && collection.push(this.mergeModelWithRecord(model, record)) @@ -285,7 +284,7 @@ export default class Query { * Instantiate new models with the given record. */ private hydrateRecord(record: Record): M { - return new this.model(record) + return this.model.$newInstance(record) } /** diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 0b30491f..a791b37e 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -1,7 +1,7 @@ import { Store } from 'vuex' +import { Constructor } from '../types' import * as Data from '../data/Data' import Model from '../model/Model' -import Constructor from '../model/Constructor' import Interpretation from '../interpretation/Interpretation' import Query from '../query/Query' @@ -21,14 +21,14 @@ export default class Repository { /** * The model object. */ - model: Constructor + model: M /** * Create a new repository instance. */ - constructor(store: Store, model: Constructor) { + constructor(store: Store, model: M | Constructor) { this.store = store - this.model = model + this.model = model instanceof Model ? model : new model() } /** diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index ceaabee6..e978e171 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -1,18 +1,17 @@ import { schema as Normalizr } from 'normalizr' import { Record } from '../data/Data' import Model from '../model/Model' -import Constructor from '../model/Constructor' export default class Schema { /** * The model instance. */ - model: Constructor + model: M /** * Create a new schema instance. */ - constructor(model: Constructor) { + constructor(model: M) { this.model = model } @@ -29,7 +28,7 @@ export default class Schema { * Create a new normalizr entity. */ private newEntity(): Normalizr.Entity { - const entity = this.model.entity + const entity = this.model.$entity const idAttribute = this.idArrtibute.bind(this) return new Normalizr.Entity(entity, {}, { idAttribute }) @@ -43,6 +42,6 @@ export default class Schema { _parentRecord: Record, _key: string ): string { - return this.model.getIndexId(record) + return this.model.$getIndexId(record) } } diff --git a/src/store/Store.ts b/src/store/Store.ts index b14839ff..a2920267 100644 --- a/src/store/Store.ts +++ b/src/store/Store.ts @@ -1,7 +1,7 @@ +import { Constructor } from '../types' import { Store, Plugin } from 'vuex' import Database from '../database/Database' import Model from '../model/Model' -import Constructor from '../model/Constructor' import Repository from '../repository/Repository' export interface Options { diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..73ebf339 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export type Constructor = new (...args: any[]) => T diff --git a/src/types/vuex.d.ts b/src/types/vuex.d.ts index 00219ae5..87bdc1be 100644 --- a/src/types/vuex.d.ts +++ b/src/types/vuex.d.ts @@ -1,6 +1,6 @@ +import { Constructor } from '../types' import Database from '../database/Database' import Model from '../model/Model' -import Constructor from '../model/Constructor' import Repository from '../repository/Repository' declare module 'vuex' { diff --git a/test/feature/relations/has_one_insert.spec.ts b/test/feature/relations/has_one_insert.spec.ts new file mode 100644 index 00000000..6e54b835 --- /dev/null +++ b/test/feature/relations/has_one_insert.spec.ts @@ -0,0 +1,44 @@ +import { createStore, assertState } from 'test/Helpers' +import { Model, Attr, Str, HasOne } from '@/index' + +describe('feature/relations/has_one_insert', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + + @HasOne(() => Phone, 'userId') + phone!: Phone | null + } + + class Phone extends Model { + static entity = 'phones' + + @Attr() id!: number + @Attr() userId!: number + @Str('') number!: string + } + + it('inserts a record to the store with "has one" relation', async () => { + const store = createStore([User, Phone]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe', + phone: { + id: 1, + number: '123-4567-8912' + } + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + phones: { + 1: { id: 1, number: '123-4567-8912' } + } + }) + }) +}) diff --git a/test/unit/model/Model_Attrs_Boolean.spec.ts b/test/unit/model/Model_Attrs_Boolean.spec.ts index 2ae23ccf..c9c8e57c 100644 --- a/test/unit/model/Model_Attrs_Boolean.spec.ts +++ b/test/unit/model/Model_Attrs_Boolean.spec.ts @@ -1,4 +1,4 @@ -import Bool from '@/model/decorators/types/Bool' +import Bool from '@/model/decorators/attributes/types/Bool' import Model from '@/model/Model' describe('unit/model/Model_Attrs_Boolean', () => { diff --git a/test/unit/model/Model_Attrs_Number.spec.ts b/test/unit/model/Model_Attrs_Number.spec.ts index 58a1da71..012c5ee1 100644 --- a/test/unit/model/Model_Attrs_Number.spec.ts +++ b/test/unit/model/Model_Attrs_Number.spec.ts @@ -1,4 +1,4 @@ -import Num from '@/model/decorators/types/Num' +import Num from '@/model/decorators/attributes/types/Num' import Model from '@/model/Model' describe('unit/model/Model_Attrs_Number', () => { diff --git a/test/unit/model/Model_Attrs_String.spec.ts b/test/unit/model/Model_Attrs_String.spec.ts index b2b29ad0..3e1162e8 100644 --- a/test/unit/model/Model_Attrs_String.spec.ts +++ b/test/unit/model/Model_Attrs_String.spec.ts @@ -1,4 +1,4 @@ -import Str from '@/model/decorators/types/Str' +import Str from '@/model/decorators/attributes/types/Str' import Model from '@/model/Model' describe('unit/model/Model_Attrs_String', () => { From 88d8eb18e18ec35f56fffa943605269f9d35485a Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 22:00:01 +0900 Subject: [PATCH 44/56] feat: add has one relationship and insert feature --- jest.config.js | 3 + package.json | 2 +- src/index.ts | 2 + src/interpretation/Interpretation.ts | 61 +++++- src/model/Model.ts | 191 ++++++++++++++---- src/model/attributes/Attribute.ts | 6 +- src/model/attributes/relations/HasOne.ts | 31 ++- src/model/attributes/relations/Relation.ts | 24 ++- src/model/attributes/types/Attr.ts | 3 +- src/model/attributes/types/Boolean.ts | 4 +- src/model/attributes/types/Number.ts | 4 +- src/model/attributes/types/String.ts | 4 +- src/model/attributes/types/Type.ts | 6 +- .../decorators/attributes/relations/HasOne.ts | 17 ++ src/model/decorators/attributes/types/Attr.ts | 2 +- src/model/decorators/attributes/types/Bool.ts | 12 +- src/model/decorators/attributes/types/Num.ts | 12 +- src/model/decorators/attributes/types/Str.ts | 12 +- src/schema/Schema.ts | 73 +++++-- src/support/Utils.ts | 4 + .../{ => types}/has_one_insert.spec.ts | 27 ++- test/setup.ts | 5 + 22 files changed, 404 insertions(+), 101 deletions(-) create mode 100644 src/model/decorators/attributes/relations/HasOne.ts rename test/feature/relations/{ => types}/has_one_insert.spec.ts (59%) create mode 100644 test/setup.ts diff --git a/jest.config.js b/jest.config.js index 70eb10d1..f6f8da17 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,6 +10,9 @@ module.exports = { }, testMatch: ['/test/**/*.spec.ts'], testPathIgnorePatterns: ['/node_modules/'], + setupFilesAfterEnv: [ + './test/setup.ts' + ], coverageDirectory: 'coverage', coverageReporters: ['json', 'lcov', 'text-summary', 'clover'], collectCoverageFrom: [ diff --git a/package.json b/package.json index 9d5b98d8..a2e47def 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "build": "node scripts/build.js", "clean": "rm -rf dist && rm -rf dist && rm -rf coverage && rm -rf .nyc_output && rm -rf .tmp", - "lint": "prettier --write --parser typescript \"{src,test}/**/*.ts\"", + "lint": "prettier --check --write --parser typescript \"{src,test}/**/*.ts\"", "lint:fail": "prettier --check --parser typescript \"{src,test}/**/*.ts\"", "test": "jest --testPathIgnorePatterns test/performance", "test:watch": "jest --testPathIgnorePatterns test/performance --watch", diff --git a/src/index.ts b/src/index.ts index 47564dfa..a466ecc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import Attr from './model/decorators/attributes/types/Attr' import Str from './model/decorators/attributes/types/Str' import Num from './model/decorators/attributes/types/Num' import Bool from './model/decorators/attributes/types/Bool' +import HasOne from './model/decorators/attributes/relations/HasOne' import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' @@ -34,6 +35,7 @@ export { Str, Num, Bool, + HasOne, RootModule, RootState, Module, diff --git a/src/interpretation/Interpretation.ts b/src/interpretation/Interpretation.ts index 2577e862..84bc9333 100644 --- a/src/interpretation/Interpretation.ts +++ b/src/interpretation/Interpretation.ts @@ -1,7 +1,8 @@ import { normalize, schema as Normalizr } from 'normalizr' import { Store } from 'vuex' import { isArray, isEmpty } from '../support/Utils' -import { Record, NormalizedData } from '../data/Data' +import { Record, Records, NormalizedData } from '../data/Data' +import Relation from '../model/attributes/relations/Relation' import Model from '../model/Model' export default class Interpretation { @@ -31,10 +32,17 @@ export default class Interpretation { return {} } - const schema = this.getSchema() + const normalizedData = this.normalize(data) - return normalize(data, isArray(data) ? [schema] : schema) - .entities as NormalizedData + this.attach(normalizedData) + + return normalizedData + } + + private normalize(data: Record | Record[]): NormalizedData { + const schema = isArray(data) ? [this.getSchema()] : this.getSchema() + + return normalize(data, schema).entities as NormalizedData } /** @@ -43,4 +51,49 @@ export default class Interpretation { private getSchema(): Normalizr.Entity { return this.store.$database.getSchema(this.model.$entity) } + + /** + * Attach any missing foreign keys to the data. + */ + private attach(data: NormalizedData): void { + for (const entity in data) { + this.attachRecords(entity, data[entity], data) + } + } + + /** + * Attach any missing foreign key to the records. + */ + private attachRecords( + entity: string, + records: Records, + data: NormalizedData + ): void { + const model = this.store.$database.getModel(entity) + + if (!model.$hasRelation()) { + return + } + + for (const id in records) { + this.attachRecord(model, records[id], data) + } + } + + /** + * Attach any missing foreign key to the record. + */ + private attachRecord( + model: Model, + record: Record, + data: NormalizedData + ): void { + for (const key in record) { + const attr = model.$fields[key] + + if (attr instanceof Relation) { + attr.attach(record[key], record, data) + } + } + } } diff --git a/src/model/Model.ts b/src/model/Model.ts index 9c73c990..4ef77b65 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,9 +1,22 @@ import { Store } from 'vuex' +import { isObject } from '../support/Utils' import { Record } from '../data/Data' import * as Attributes from './attributes/Attributes' -export interface Fields { - [key: string]: Attributes.Attribute +export interface Fields { + [key: string]: Attributes.Attribute +} + +interface Schemas { + [name: string]: Fields +} + +interface Registries { + [name: string]: Registry +} + +interface Registry { + [key: string]: () => Attributes.Attribute } export default class Model { @@ -26,63 +39,117 @@ export default class Model { * The schema for the model. It contains the result of the `fields` * method or the attributes defined by decorators. */ - protected static schema: Fields = {} + protected static schemas: Schemas = {} + + /** + * The registry for the model. It contains predefined model schema generated + * by the decorators, and gets evaluated and stored at `schema` property + * when registering models to the database + */ + protected static registries: Registries = {} + + /** + * The array of booted models. + */ + protected static booted: { [name: string]: boolean } = {} /** * Create a new model instance. */ constructor(attributes?: Record) { + this.$boot() + this.$fill(attributes) } /** - * Set the attribute to the schema. + * Build a schema by evaluating fields and registry. */ - static setSchema( - this: M, + static initializeSchema(): void { + this.schemas[this.entity] = {} + + const registry = this.registries[this.entity] + + for (const key in registry) { + this.schemas[this.entity][key] = registry[key]() + } + } + + /** + * Set the attribute to the registry. + */ + static setRegistry( key: string, - attribute: Attributes.Attribute - ): M { - this.schema[key] = attribute + attribute: () => Attributes.Attribute + ): typeof Model { + if (!this.registries[this.entity]) { + this.registries[this.entity] = {} + } + + this.registries[this.entity][key] = attribute return this } + /** + * Clear the list of booted models so they will be re-booted. + */ + static clearBootedModels(): void { + this.booted = {} + this.schemas = {} + } + /** * Create a new attr attribute instance. */ - static attr(this: M, value: any): Attributes.Attr { - return new Attributes.Attr(this, value) + static attr(value: any): Attributes.Attr { + return new Attributes.Attr(new this(), value) } /** * Create a new string attribute instance. */ - static string( - this: M, - value: string | null - ): Attributes.String { - return new Attributes.String(this, value) + static string(value: string | null): Attributes.String { + return new Attributes.String(new this(), value) } /** * Create a new number attribute instance. */ - static number( - this: M, - value: number | null - ): Attributes.Number { - return new Attributes.Number(this, value) + static number(value: number | null): Attributes.Number { + return new Attributes.Number(new this(), value) } /** * Create a new boolean attribute instance. */ - static boolean( - this: M, - value: boolean | null - ): Attributes.Boolean { - return new Attributes.Boolean(this, value) + static boolean(value: boolean | null): Attributes.Boolean { + return new Attributes.Boolean(new this(), value) + } + + /** + * Create a new has one relation instance. + */ + static hasOne( + related: typeof Model, + foreignKey: string, + localKey?: string + ): Attributes.HasOne { + localKey = localKey ?? this.getLocalKey() + + return new Attributes.HasOne( + new this(), + new related(), + foreignKey, + localKey + ) + } + + /** + * Get the local key for the model. + */ + static getLocalKey(): string { + return this.primaryKey } /** @@ -128,16 +195,36 @@ export default class Model { * particularly useful during the hydration of new objects via the query. */ $newInstance(attributes: Record): this { - const model = this.$self as any + const model = new this.$self(attributes) as this + + model.$setStore(model.$store) - return new model(attributes) + return model } /** * Get model fields for the model. */ - get $fields(): Fields { - return this.$self.schema + get $fields(): Fields { + return this.$self.schemas[this.$entity] + } + + /** + * Bootstrap the model. + */ + protected $boot(): void { + if (!this.$self.booted[this.$entity]) { + this.$self.booted[this.$entity] = true + + this.$initializeSchema() + } + } + + /** + * Build a schema by evaluating fields and registry. + */ + protected $initializeSchema(): void { + this.$self.initializeSchema() } /** @@ -146,15 +233,22 @@ export default class Model { */ $fill(attributes: Record = {}): this { for (const key in this.$fields) { - this.$fillField(key, attributes[key]) + const attr = this.$fields[key] + const value = attributes[key] + + attr instanceof Attributes.Relation + ? this.$fillRelationFields(key, attr, value) + : this.$fillTypeField(key, attr, value) } return this } - private $fillField(key: string, value: any): void { - const attr = this.$fields[key] - + protected $fillTypeField( + key: string, + attr: Attributes.Attribute, + value: any + ): void { if (value !== undefined) { this[key] = attr.make(value) return @@ -168,6 +262,16 @@ export default class Model { this[key] = attr.make() } + protected $fillRelationFields( + key: string, + attr: Attributes.Relation, + value: any + ): void { + if (isObject(value)) { + this[key] = attr.make(value) + } + } + /** * Get the index id for the model or the given record. */ @@ -184,9 +288,26 @@ export default class Model { const record = {} as Record for (const key in this.$fields) { - record[key] = this[key] + if (this[key] !== undefined) { + record[key] = this[key] + } } return record } + + /** + * Check if the model has any relations defined in the schema. + */ + $hasRelation(): boolean { + let result = false + + for (const key in this.$fields) { + if (this.$fields[key] instanceof Attributes.Relation) { + result = true + } + } + + return result + } } diff --git a/src/model/attributes/Attribute.ts b/src/model/attributes/Attribute.ts index 25752059..e11adbae 100644 --- a/src/model/attributes/Attribute.ts +++ b/src/model/attributes/Attribute.ts @@ -1,15 +1,15 @@ import Model from '../Model' -export default abstract class Attribute { +export default abstract class Attribute { /** * The model instance. */ - protected model: M + protected model: Model /** * Create a new attribute instance. */ - constructor(model: M) { + constructor(model: Model) { this.model = model } diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index eb63f2f2..a943f058 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -1,10 +1,10 @@ +import { Schema as NormalizrSchema } from 'normalizr' +import Schema from '../../../schema/Schema' +import { Record, NormalizedData } from '../../../data/Data' import Model from '../../Model' import Relation from './Relation' -export default class String< - P extends typeof Model, - R extends typeof Model -> extends Relation { +export default class HasOne extends Relation { /** * The foreign key of the parent model. */ @@ -18,12 +18,33 @@ export default class String< /** * Create a new has one relation instance. */ - constructor(parent: P, related: R, foreignKey: string, localKey: string) { + constructor( + parent: Model, + related: Model, + foreignKey: string, + localKey: string + ) { super(parent, related) this.foreignKey = foreignKey this.localKey = localKey } + /** + * Define the normalizr schema for the relationship. + */ + define(schema: Schema): NormalizrSchema { + return schema.one(this.related) + } + + /** + * Attach the relational key to the given data. + */ + attach(id: string | number, record: Record, data: NormalizedData): void { + const relatedRecord = data[this.related.$entity][id] + + relatedRecord[this.foreignKey] = record[this.localKey] + } + /** * Make the value for the attribute. */ diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts index 0adb899e..e5d06c31 100644 --- a/src/model/attributes/relations/Relation.ts +++ b/src/model/attributes/relations/Relation.ts @@ -1,26 +1,36 @@ +import { Schema as NormalizrSchema } from 'normalizr' +import Schema from '../../../schema/Schema' +import { Record, NormalizedData } from '../../../data/Data' import Model from '../../Model' import Attribute from '../Attribute' -export default abstract class Relelation< - P extends typeof Model, - R extends typeof Model -> extends Attribute

{ +export default abstract class Relelation extends Attribute { /** * The parent model. */ - protected parent: P + protected parent: Model /** * The related model. */ - protected related: R + protected related: Model /** * Create a new relation instance. */ - constructor(parent: P, related: R) { + constructor(parent: Model, related: Model) { super(parent) this.parent = parent this.related = related } + + /** + * Define the normalizr schema for the relationship. + */ + abstract define(schema: Schema): NormalizrSchema + + /** + * Attach the relational key to the given data. + */ + abstract attach(ids: any, record: Record, data: NormalizedData): void } diff --git a/src/model/attributes/types/Attr.ts b/src/model/attributes/types/Attr.ts index 4e0d444f..dd4b8dc5 100644 --- a/src/model/attributes/types/Attr.ts +++ b/src/model/attributes/types/Attr.ts @@ -1,7 +1,6 @@ -import Model from '../../Model' import Type from './Type' -export default class Attr extends Type { +export default class Attr extends Type { /** * Make the value for the attribute. */ diff --git a/src/model/attributes/types/Boolean.ts b/src/model/attributes/types/Boolean.ts index 8b639a87..d21e563a 100644 --- a/src/model/attributes/types/Boolean.ts +++ b/src/model/attributes/types/Boolean.ts @@ -1,11 +1,11 @@ import Model from '../../Model' import Type from './Type' -export default class Boolean extends Type { +export default class Boolean extends Type { /** * Create a new boolean attribute instance. */ - constructor(model: M, value: boolean | null) { + constructor(model: Model, value: boolean | null) { super(model, value) } diff --git a/src/model/attributes/types/Number.ts b/src/model/attributes/types/Number.ts index c9677cee..755f5bf7 100644 --- a/src/model/attributes/types/Number.ts +++ b/src/model/attributes/types/Number.ts @@ -1,11 +1,11 @@ import Model from '../../Model' import Type from './Type' -export default class Number extends Type { +export default class Number extends Type { /** * Create a new number attribute instance. */ - constructor(model: M, value: number | null) { + constructor(model: Model, value: number | null) { super(model, value) } diff --git a/src/model/attributes/types/String.ts b/src/model/attributes/types/String.ts index 61472b61..52e2d006 100644 --- a/src/model/attributes/types/String.ts +++ b/src/model/attributes/types/String.ts @@ -1,11 +1,11 @@ import Model from '../../Model' import Type from './Type' -export default class String extends Type { +export default class String extends Type { /** * Create a new string attribute instance. */ - constructor(model: M, value: string | null) { + constructor(model: Model, value: string | null) { super(model, value) } diff --git a/src/model/attributes/types/Type.ts b/src/model/attributes/types/Type.ts index 558e8e18..2ef34718 100644 --- a/src/model/attributes/types/Type.ts +++ b/src/model/attributes/types/Type.ts @@ -1,9 +1,7 @@ import Model from '../../Model' import Attribute from '../Attribute' -export default abstract class Type extends Attribute< - M -> { +export default abstract class Type extends Attribute { /** * The default value for the attribute. */ @@ -17,7 +15,7 @@ export default abstract class Type extends Attribute< /** * Create a new type attribute instance. */ - constructor(model: M, value: any = null) { + constructor(model: Model, value: any = null) { super(model) this.value = value } diff --git a/src/model/decorators/attributes/relations/HasOne.ts b/src/model/decorators/attributes/relations/HasOne.ts new file mode 100644 index 00000000..e2f6cc3e --- /dev/null +++ b/src/model/decorators/attributes/relations/HasOne.ts @@ -0,0 +1,17 @@ +import Model from '../../../Model' +import { PropertyDecorator } from '../../Contracts' + +/** + * Create a has one decorator. + */ +export default function HasOne( + related: () => typeof Model, + foreignKey: string, + localKey?: string +): PropertyDecorator { + return (target, propertyKey) => { + target.$self.setRegistry(propertyKey, () => + target.$self.hasOne(related(), foreignKey, localKey) + ) + } +} diff --git a/src/model/decorators/attributes/types/Attr.ts b/src/model/decorators/attributes/types/Attr.ts index 2d207679..2e946b0b 100644 --- a/src/model/decorators/attributes/types/Attr.ts +++ b/src/model/decorators/attributes/types/Attr.ts @@ -5,6 +5,6 @@ import { PropertyDecorator } from '../../Contracts' */ export default function Attr(value?: any): PropertyDecorator { return (target, propertyKey) => { - target.$self.setSchema(propertyKey, target.$self.attr(value)) + target.$self.setRegistry(propertyKey, () => target.$self.attr(value)) } } diff --git a/src/model/decorators/attributes/types/Bool.ts b/src/model/decorators/attributes/types/Bool.ts index 2cc8191a..a091ffec 100644 --- a/src/model/decorators/attributes/types/Bool.ts +++ b/src/model/decorators/attributes/types/Bool.ts @@ -8,12 +8,14 @@ export default function Str( options: TypeOptions = {} ): PropertyDecorator { return (target, propertyKey) => { - const attr = target.$self.boolean(value) + target.$self.setRegistry(propertyKey, () => { + const attr = target.$self.boolean(value) - if (options.nullable) { - attr.nullable() - } + if (options.nullable) { + attr.nullable() + } - target.$self.setSchema(propertyKey, attr) + return attr + }) } } diff --git a/src/model/decorators/attributes/types/Num.ts b/src/model/decorators/attributes/types/Num.ts index d0da0ab2..78404ac8 100644 --- a/src/model/decorators/attributes/types/Num.ts +++ b/src/model/decorators/attributes/types/Num.ts @@ -8,12 +8,14 @@ export default function Str( options: TypeOptions = {} ): PropertyDecorator { return (target, propertyKey) => { - const attr = target.$self.number(value) + target.$self.setRegistry(propertyKey, () => { + const attr = target.$self.number(value) - if (options.nullable) { - attr.nullable() - } + if (options.nullable) { + attr.nullable() + } - target.$self.setSchema(propertyKey, attr) + return attr + }) } } diff --git a/src/model/decorators/attributes/types/Str.ts b/src/model/decorators/attributes/types/Str.ts index b35711a0..763c1775 100644 --- a/src/model/decorators/attributes/types/Str.ts +++ b/src/model/decorators/attributes/types/Str.ts @@ -8,12 +8,14 @@ export default function Str( options: TypeOptions = {} ): PropertyDecorator { return (target, propertyKey) => { - const attr = target.$self.string(value) + target.$self.setRegistry(propertyKey, () => { + const attr = target.$self.string(value) - if (options.nullable) { - attr.nullable() - } + if (options.nullable) { + attr.nullable() + } - target.$self.setSchema(propertyKey, attr) + return attr + }) } } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index e978e171..d87786ea 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -1,35 +1,63 @@ -import { schema as Normalizr } from 'normalizr' -import { Record } from '../data/Data' +import { schema as Normalizr, Schema as NormalizrSchema } from 'normalizr' +import { Relation } from '../model/attributes/Attributes' import Model from '../model/Model' -export default class Schema { +interface Schemas { + [entity: string]: Normalizr.Entity +} + +export default class Schema { + /** + * List of generated schemas. + */ + private schemas: Schemas = {} + /** * The model instance. */ - model: M + private model: Model /** * Create a new schema instance. */ - constructor(model: M) { + constructor(model: Model) { this.model = model } /** * Create a single schema. */ - one(): Normalizr.Entity { - const schema = this.newEntity() + one(model?: Model): Normalizr.Entity { + model = model || this.model + + if (this.schemas[model.$entity]) { + return this.schemas[model.$entity] + } + + const schema = this.newEntity(model) + + this.schemas[model.$entity] = schema + + const definition = this.definition(model) + + schema.define(definition) return schema } + /** + * Create an array schema for the given model. + */ + many(model: Model): Normalizr.Array { + return new Normalizr.Array(this.one(model)) + } + /** * Create a new normalizr entity. */ - private newEntity(): Normalizr.Entity { - const entity = this.model.$entity - const idAttribute = this.idArrtibute.bind(this) + private newEntity(model: Model): Normalizr.Entity { + const entity = model.$entity + const idAttribute = this.idAttribute(model) return new Normalizr.Entity(entity, {}, { idAttribute }) } @@ -37,11 +65,24 @@ export default class Schema { /** * The id attribute option for the normalizr entity. */ - private idArrtibute( - record: Record, - _parentRecord: Record, - _key: string - ): string { - return this.model.$getIndexId(record) + private idAttribute(model: Model): Normalizr.StrategyFunction { + return (record) => model.$getIndexId(record) + } + + /** + * Create a dfinition for the given model. + */ + private definition(model: Model): NormalizrSchema { + const definition: NormalizrSchema = {} + + for (const key in model.$fields) { + const field = model.$fields[key] + + if (field instanceof Relation) { + definition[key] = field.define(this) + } + } + + return definition } } diff --git a/src/support/Utils.ts b/src/support/Utils.ts index 0eb430f6..186f21b6 100644 --- a/src/support/Utils.ts +++ b/src/support/Utils.ts @@ -4,6 +4,10 @@ interface SortableArray { value: T } +export function isObject(value: any): value is Record { + return value !== null && typeof value === 'object' +} + export function isArray(value: any): value is any[] { return Array.isArray(value) } diff --git a/test/feature/relations/has_one_insert.spec.ts b/test/feature/relations/types/has_one_insert.spec.ts similarity index 59% rename from test/feature/relations/has_one_insert.spec.ts rename to test/feature/relations/types/has_one_insert.spec.ts index 6e54b835..9cb2f89d 100644 --- a/test/feature/relations/has_one_insert.spec.ts +++ b/test/feature/relations/types/has_one_insert.spec.ts @@ -1,7 +1,7 @@ import { createStore, assertState } from 'test/Helpers' import { Model, Attr, Str, HasOne } from '@/index' -describe('feature/relations/has_one_insert', () => { +describe('feature/relations/types/has_one_insert', () => { class User extends Model { static entity = 'users' @@ -28,6 +28,7 @@ describe('feature/relations/has_one_insert', () => { name: 'John Doe', phone: { id: 1, + userId: 1, number: '123-4567-8912' } }) @@ -37,7 +38,29 @@ describe('feature/relations/has_one_insert', () => { 1: { id: 1, name: 'John Doe' } }, phones: { - 1: { id: 1, number: '123-4567-8912' } + 1: { id: 1, userId: 1, number: '123-4567-8912' } + } + }) + }) + + it('generates missing foreign key', async () => { + const store = createStore([User, Phone]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe', + phone: { + id: 1, + number: '123-4567-8912' + } + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + phones: { + 1: { id: 1, userId: 1, number: '123-4567-8912' } } }) }) diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 00000000..6c5aa66e --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,5 @@ +import { Model } from '@/index' + +beforeEach(() => { + Model.clearBootedModels() +}) From 0151a0d3d809f535d451a5e82f591ee410bfdfa9 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Wed, 8 Apr 2020 22:52:14 +0900 Subject: [PATCH 45/56] fix: missing relational key in has one relation insert not working --- src/model/attributes/relations/HasOne.ts | 6 ++-- .../relations/types/has_one_insert.spec.ts | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index a943f058..8d9ac7b1 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -40,9 +40,11 @@ export default class HasOne extends Relation { * Attach the relational key to the given data. */ attach(id: string | number, record: Record, data: NormalizedData): void { - const relatedRecord = data[this.related.$entity][id] + const relatedRecord = data[this.related.$entity]?.[id] - relatedRecord[this.foreignKey] = record[this.localKey] + if (relatedRecord) { + relatedRecord[this.foreignKey] = record[this.localKey] + } } /** diff --git a/test/feature/relations/types/has_one_insert.spec.ts b/test/feature/relations/types/has_one_insert.spec.ts index 9cb2f89d..8d059098 100644 --- a/test/feature/relations/types/has_one_insert.spec.ts +++ b/test/feature/relations/types/has_one_insert.spec.ts @@ -64,4 +64,37 @@ describe('feature/relations/types/has_one_insert', () => { } }) }) + + it('can insert a record with missing relational key', async () => { + const store = createStore([User, Phone]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe' + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + phones: {} + }) + }) + + it('can insert a record with relational key set to `null`', async () => { + const store = createStore([User, Phone]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe', + phone: null + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + phones: {} + }) + }) }) From 3dfd5c8cf554cf780ec32a1e7a59987a3c5be459 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Wed, 8 Apr 2020 23:38:23 +0100 Subject: [PATCH 46/56] fix(Relation): class name typo --- src/model/attributes/relations/Relation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts index e5d06c31..4a78c49d 100644 --- a/src/model/attributes/relations/Relation.ts +++ b/src/model/attributes/relations/Relation.ts @@ -4,7 +4,7 @@ import { Record, NormalizedData } from '../../../data/Data' import Model from '../../Model' import Attribute from '../Attribute' -export default abstract class Relelation extends Attribute { +export default abstract class Relation extends Attribute { /** * The parent model. */ From 72c7477ecc0fcc0230a7b1550ba6e1988b0fe615 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 10:29:11 +0900 Subject: [PATCH 47/56] feat: add has one retrieve feature --- src/data/Data.ts | 4 +- src/model/Model.ts | 48 ++++-- src/model/attributes/relations/HasOne.ts | 39 ++++- src/model/attributes/relations/Relation.ts | 62 ++++++- src/query/Query.ts | 163 ++++++++++++++++-- src/query/options/Options.ts | 8 + src/repository/Repository.ts | 25 ++- test/Helpers.ts | 4 + .../relations/types/has_one_retrieve.spec.ts | 75 ++++++++ 9 files changed, 385 insertions(+), 43 deletions(-) create mode 100644 test/feature/relations/types/has_one_retrieve.spec.ts diff --git a/src/data/Data.ts b/src/data/Data.ts index d7ead5d9..781fa0c6 100644 --- a/src/data/Data.ts +++ b/src/data/Data.ts @@ -12,9 +12,9 @@ export interface NormalizedData { [entity: string]: Records } -export type Item = M | null +export type Item = M | null -export type Collection = M[] +export type Collection = M[] export interface Collections { [name: string]: Collection diff --git a/src/model/Model.ts b/src/model/Model.ts index 4ef77b65..1b848b8b 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -282,32 +282,56 @@ export default class Model { } /** - * Get the serialized model attributes. + * Check if the model has any relations defined in the schema. */ - $getAttributes(): Record { - const record = {} as Record + $hasRelation(): boolean { + let result = false for (const key in this.$fields) { - if (this[key] !== undefined) { - record[key] = this[key] + if (this.$fields[key] instanceof Attributes.Relation) { + result = true } } - return record + return result } /** - * Check if the model has any relations defined in the schema. + * Get the relation instance for the given relation name. */ - $hasRelation(): boolean { - let result = false + $getRelation(name: string): Attributes.Relation { + const relation = this.$fields[name] + + if (!(relation instanceof Attributes.Relation)) { + throw new Error( + `[Vuex ORM] Relationship [${name}] on model [${this.$entity}] not found.` + ) + } + + return relation + } + + /** + * Set the given relationship on the model. + */ + $setRelation(relation: string, model: Model | Model[] | null): this { + this[relation] = model + + return this + } + + /** + * Get the serialized model attributes. + */ + $getAttributes(): Record { + const record = {} as Record for (const key in this.$fields) { - if (this.$fields[key] instanceof Attributes.Relation) { - result = true + if (this[key] !== undefined) { + record[key] = this[key] } } - return result + return record } } diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index 8d9ac7b1..bd060c9b 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -1,8 +1,9 @@ import { Schema as NormalizrSchema } from 'normalizr' import Schema from '../../../schema/Schema' -import { Record, NormalizedData } from '../../../data/Data' +import { Record, NormalizedData, Collection } from '../../../data/Data' +import Query from '../../../query/Query' import Model from '../../Model' -import Relation from './Relation' +import Relation, { Dictionaly } from './Relation' export default class HasOne extends Relation { /** @@ -47,6 +48,40 @@ export default class HasOne extends Relation { } } + /** + * Set the constraints for an eager load of the relation. + */ + addEagerConstraints(query: Query, models: Collection): void { + query.whereIn(this.foreignKey, this.getKeys(models, this.localKey)) + } + + /** + * Match the eagerly loaded results to their parents. + */ + match(relation: string, models: Collection, results: Collection): void { + const dictionary = this.buildDictionary(results) + + models.map((model) => { + const key = model[this.localKey] + + dictionary[key] + ? model.$setRelation(relation, dictionary[key][0]) + : model.$setRelation(relation, null) + }) + } + + /** + * Build model dictionary keyed by the relation's foreign key. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array + */ + protected buildDictionary(results: Collection): Dictionaly { + return this.mapToDictionary(results, (result) => { + return [result[this.foreignKey], result] + }) + } + /** * Make the value for the attribute. */ diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts index 4a78c49d..89dc9f95 100644 --- a/src/model/attributes/relations/Relation.ts +++ b/src/model/attributes/relations/Relation.ts @@ -1,9 +1,14 @@ import { Schema as NormalizrSchema } from 'normalizr' import Schema from '../../../schema/Schema' -import { Record, NormalizedData } from '../../../data/Data' +import { Record, NormalizedData, Collection } from '../../../data/Data' +import Query from '../../../query/Query' import Model from '../../Model' import Attribute from '../Attribute' +export interface Dictionaly { + [id: string]: Model[] +} + export default abstract class Relation extends Attribute { /** * The parent model. @@ -24,6 +29,13 @@ export default abstract class Relation extends Attribute { this.related = related } + /** + * Get the related model of the relation. + */ + public getRelated(): Model { + return this.related + } + /** * Define the normalizr schema for the relationship. */ @@ -33,4 +45,52 @@ export default abstract class Relation extends Attribute { * Attach the relational key to the given data. */ abstract attach(ids: any, record: Record, data: NormalizedData): void + + /** + * Set the constraints for an eager load of the relation. + */ + abstract addEagerConstraints(query: Query, models: Collection): void + + /** + * Get the relationship for eager loading. + */ + public getEager(query: Query): Collection { + return query.get() + } + + /** + * Match the eagerly loaded results to their parents. + */ + abstract match( + relation: string, + models: Collection, + results: Collection + ): void + + /** + * Get all of the primary keys for an array of models. + */ + protected getKeys(models: Collection, key: string) { + return models.map((model) => model[key]) + } + + /** + * Run a dictionary map over the items. + */ + protected mapToDictionary( + models: Collection, + callback: (model: Model) => [string, Model] + ): Dictionaly { + return models.reduce((dictionaly, model) => { + const [key, value] = callback(model) + + if (!dictionaly[key]) { + dictionaly[key] = [] + } + + dictionaly[key].push(value) + + return dictionaly + }, {}) + } } diff --git a/src/query/Query.ts b/src/query/Query.ts index e5c8bc0e..8be85a5a 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -1,25 +1,41 @@ import { Store } from 'vuex' -import { isEmpty, groupBy } from '../support/Utils' +import { isArray, isEmpty, groupBy } from '../support/Utils' import { Record, Item, Collection } from '../data/Data' +import { Relation } from '../model/attributes/Attributes' import Model from '../model/Model' import Connection from '../connection/Connection' import * as Options from './options/Options' -export default class Query { +export default class Query { /** * The store instance. */ - store: Store + protected store: Store /** * The model object. */ - model: M + protected model: M /** * The where constraints for the query. */ - wheres: Options.Where[] = [] + protected wheres: Options.Where[] = [] + + /** + * The maximum number of records to return. + */ + protected take: number | null = null + + /** + * The number of records to skip. + */ + protected skip: number = 0 + + /** + * The relationships that should be eager loaded. + */ + protected eagerLoad: Options.EagerLoad = {} /** * Create a new query instance. @@ -29,6 +45,13 @@ export default class Query { this.model = model } + /** + * Create a new query instance from the given relation. + */ + protected newQueryForRelation(relation: Relation): Query { + return new Query(this.store, relation.getRelated()) + } + /** * Create a new connection instance. */ @@ -45,6 +68,15 @@ export default class Query { return this } + /** + * Add a "where in" clause to the query. + */ + whereIn(field: string, values: any[]): this { + this.wheres.push({ field, value: values, boolean: 'and' }) + + return this + } + /** * Add an "or where" clause to the query. */ @@ -54,11 +86,51 @@ export default class Query { return this } + /** + * Set the "take" value of the query. + */ + limit(value: number): this { + this.take = value + + return this + } + + /** + * Set the "offset" value of the query. + */ + offset(value: number): this { + this.skip = value + + return this + } + + /** + * Set the relationships that should be eager loaded. + */ + with(name: string): Query { + this.eagerLoad[name] = () => {} + + return this + } + /** * Retrieve models by processing whole query chain. */ get(): Collection { - return this.select() + const models = this.select() + + if (!isEmpty(models)) { + this.eagerLoadRelations(models) + } + + return models + } + + /** + * Execute the query and get the first result. + */ + first(): Item { + return this.limit(1).get()[0] } /** @@ -118,6 +190,7 @@ export default class Query { let models = this.getModels() models = this.filterWhere(models) + models = this.filterLimit(models) return models } @@ -125,7 +198,7 @@ export default class Query { /** * Filter the given collection by the registered where clause. */ - private filterWhere(models: Collection): Collection { + protected filterWhere(models: Collection): Collection { if (isEmpty(this.wheres)) { return models } @@ -138,7 +211,7 @@ export default class Query { /** * Get comparator for the where clause. */ - private getWhereComparator(): (model: M) => boolean { + protected getWhereComparator(): (model: M) => boolean { const { and, or } = groupBy(this.wheres, (where) => where.boolean) return (model) => { @@ -154,10 +227,64 @@ export default class Query { /** * The function to compare where clause to the given model. */ - private whereComparator(model: M, where: Options.Where): boolean { + protected whereComparator(model: M, where: Options.Where): boolean { + if (isArray(where.value)) { + return where.value.includes(model[where.field]) + } + return model[where.field] === where.value } + /** + * Filter the given collection by the registered limit and offset values. + */ + protected filterLimit(models: Collection): Collection { + return this.take !== null + ? models.slice(this.skip, this.skip + this.take) + : models.slice(this.skip) + } + + /** + * Eager load the relationships for the models. + */ + protected eagerLoadRelations(models: Collection): void { + for (const name in this.eagerLoad) { + this.eagerLoadRelation(models, name, this.eagerLoad[name]) + } + } + + /** + * Eagerly load the relationship on a set of models. + */ + protected eagerLoadRelation( + models: Collection, + name: string, + constraints: Options.EagerLoadConstraint + ): void { + // First we will "back up" the existing where conditions on the query so we can + // add our eager constraints. Then we will merge the wheres that were on the + // query back to it in order that any where conditions might be specified. + const relation = this.getRelation(name) + + const query = this.newQueryForRelation(relation) + + relation.addEagerConstraints(query, models) + + constraints(query) + + // Once we have the results, we just match those back up to their parent models + // using the relationship instance. Then we just return the finished arrays + // of models which have been eagerly hydrated and are readied for return. + relation.match(name, models, relation.getEager(query)) + } + + /** + * Get the relation instance for the given relation name. + */ + protected getRelation(name: string): Relation { + return this.model.$getRelation(name) + } + /** * Insert the given record to the store. */ @@ -184,7 +311,7 @@ export default class Query { * Get models by merging the records. This method will use the primary key * in the records to fetch models and merge the given record to the model. */ - private getMergedModels(records: Record[]): Collection { + protected getMergedModels(records: Record[]): Collection { return records.reduce>((collection, record) => { const model = this.find(this.model.$getIndexId(record)) @@ -260,7 +387,7 @@ export default class Query { /** * Get an array of ids from the given collection. */ - private getIdsFromCollection(models: Collection): string[] { + protected getIdsFromCollection(models: Collection): string[] { return models.map((model) => model.$getIndexId()) } @@ -268,7 +395,7 @@ export default class Query { * Normalize the given index id. This method will convert the given key to * the string since the index key must be a string. */ - private normalizeIndexId(id: string | number): string { + protected normalizeIndexId(id: string | number): string { return String(id) } @@ -276,42 +403,42 @@ export default class Query { * Normalize the given index ids. This method will convert the given key to * the string since the index key must be a string. */ - private normalizeIndexIds(ids: (string | number)[]): string[] { + protected normalizeIndexIds(ids: (string | number)[]): string[] { return ids.map((id) => String(id)) } /** * Instantiate new models with the given record. */ - private hydrateRecord(record: Record): M { + protected hydrateRecord(record: Record): M { return this.model.$newInstance(record) } /** * Instantiate new models with the given collection of records. */ - private hydrateRecords(records: Record[]): Collection { + protected hydrateRecords(records: Record[]): Collection { return records.map((record) => this.hydrateRecord(record)) } /** * Convert all models into the plain record. */ - private dehydrateModels(models: Collection): Record[] { + protected dehydrateModels(models: Collection): Record[] { return models.map((model) => model.$getAttributes()) } /** * Merge the model with the given record. */ - private mergeModelWithRecord(model: M, record: Record): M { + protected mergeModelWithRecord(model: M, record: Record): M { return model.$fill(record) } /** * Merge models with the given record. */ - private mergeModelsWithRecord( + protected mergeModelsWithRecord( models: Collection, record: Record ): Collection { diff --git a/src/query/options/Options.ts b/src/query/options/Options.ts index dc9af66e..19c49f43 100644 --- a/src/query/options/Options.ts +++ b/src/query/options/Options.ts @@ -1,3 +1,5 @@ +import Query from '../Query' + export interface Where { field: string | number value: any @@ -8,3 +10,9 @@ export interface WhereGroup { and?: Where[] or?: Where[] } + +export interface EagerLoad { + [name: string]: EagerLoadConstraint +} + +export type EagerLoadConstraint = (query: Query) => void diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index a791b37e..d39bf58b 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -16,12 +16,12 @@ export default class Repository { /** * The store instance. */ - store: Store + protected store: Store /** * The model object. */ - model: M + protected model: M /** * Create a new repository instance. @@ -68,6 +68,13 @@ export default class Repository { return this.query().orWhere(field, value) } + /** + * Set the relationships that should be eager loaded. + */ + with(name: string): Query { + return this.query().with(name) + } + /** * Get all models from the store. */ @@ -99,7 +106,7 @@ export default class Repository { /** * Persist records to the store by the given method. */ - private persist( + protected persist( method: PersistMethod, record: Data.Record | Data.Record[] ): Promise { @@ -116,7 +123,7 @@ export default class Repository { /** * Persist normalized records with the given method. */ - private persistRecords( + protected persistRecords( method: PersistMethod, records: Data.Records ): Promise> { @@ -126,7 +133,7 @@ export default class Repository { /** * Create collection promises for the given normalized data. */ - private createCollectionPromises( + protected createCollectionPromises( method: PersistMethod, data: Data.NormalizedData ): CollectionPromises { @@ -147,7 +154,7 @@ export default class Repository { /** * Resolve all collection promises and create a new collections object. */ - private async resolveCollectionPromises( + protected async resolveCollectionPromises( indexes: string[], promises: Promise>[] ): Promise { @@ -163,7 +170,7 @@ export default class Repository { /** * Convert normalized data into an array of records. */ - private mapNormalizedData(records: Data.Records): Data.Record[] { + protected mapNormalizedData(records: Data.Records): Data.Record[] { const items = [] as Data.Record[] for (const id in records) { @@ -176,7 +183,9 @@ export default class Repository { /** * Normalize the given record. */ - private interpret(records: Data.Record | Data.Record[]): Data.NormalizedData { + protected interpret( + records: Data.Record | Data.Record[] + ): Data.NormalizedData { return this.interpretation().process(records) } diff --git a/test/Helpers.ts b/test/Helpers.ts index 4bb5b3f3..d1896282 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -53,6 +53,10 @@ export function assertState(store: Store, entities: Entities): void { expect(store.state.entities).toEqual(createState(entities)) } +export function assertModel(model: M, record: Record): void { + expect(model.$getAttributes()).toEqual(record) +} + export function assertModels( models: Collection, record: Record[] diff --git a/test/feature/relations/types/has_one_retrieve.spec.ts b/test/feature/relations/types/has_one_retrieve.spec.ts new file mode 100644 index 00000000..b320061e --- /dev/null +++ b/test/feature/relations/types/has_one_retrieve.spec.ts @@ -0,0 +1,75 @@ +import { createStore, fillState, assertModel } from 'test/Helpers' +import { Model, Attr, Str, HasOne } from '@/index' + +describe('feature/relations/types/has_one_retrieve', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + + @HasOne(() => Phone, 'userId') + phone!: Phone | null + } + + class Phone extends Model { + static entity = 'phones' + + @Attr() id!: number + @Attr() userId!: number + @Str('') number!: string + } + + it('can eager load has one relation', async () => { + const store = createStore([User, Phone]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + phones: { + 1: { id: 1, userId: 1, number: '123-4567-8912' } + } + }) + + const user = store + .$repo(User) + .with('phone') + .first()! + + expect(user).toBeInstanceOf(User) + expect(user.phone).toBeInstanceOf(Phone) + assertModel(user, { + id: 1, + name: 'John Doe', + phone: { + id: 1, + userId: 1, + number: '123-4567-8912' + } + }) + }) + + it('can eager load missing relation as `null`', async () => { + const store = createStore([User, Phone]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + phones: {} + }) + + const user = store + .$repo(User) + .with('phone') + .first()! + + expect(user).toBeInstanceOf(User) + assertModel(user, { + id: 1, + name: 'John Doe', + phone: null + }) + }) +}) From 052f4926959f7ae6b6c3aeb3647dd05ee9c62672 Mon Sep 17 00:00:00 2001 From: Cue <1493221+cuebit@users.noreply.github.com> Date: Thu, 9 Apr 2020 06:38:09 +0100 Subject: [PATCH 48/56] chore: fixes grammatical issues --- src/interpretation/Interpretation.ts | 4 ++-- src/model/attributes/relations/HasOne.ts | 4 ++-- src/model/attributes/relations/Relation.ts | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/interpretation/Interpretation.ts b/src/interpretation/Interpretation.ts index 84bc9333..9cdf50b2 100644 --- a/src/interpretation/Interpretation.ts +++ b/src/interpretation/Interpretation.ts @@ -62,7 +62,7 @@ export default class Interpretation { } /** - * Attach any missing foreign key to the records. + * Attach any missing foreign keys to the records. */ private attachRecords( entity: string, @@ -81,7 +81,7 @@ export default class Interpretation { } /** - * Attach any missing foreign key to the record. + * Attach any missing foreign keys to the record. */ private attachRecord( model: Model, diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index bd060c9b..c79a868d 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -3,7 +3,7 @@ import Schema from '../../../schema/Schema' import { Record, NormalizedData, Collection } from '../../../data/Data' import Query from '../../../query/Query' import Model from '../../Model' -import Relation, { Dictionaly } from './Relation' +import Relation, { Dictionary } from './Relation' export default class HasOne extends Relation { /** @@ -76,7 +76,7 @@ export default class HasOne extends Relation { * @param \Illuminate\Database\Eloquent\Collection $results * @return array */ - protected buildDictionary(results: Collection): Dictionaly { + protected buildDictionary(results: Collection): Dictionary { return this.mapToDictionary(results, (result) => { return [result[this.foreignKey], result] }) diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts index 89dc9f95..9c2f9e41 100644 --- a/src/model/attributes/relations/Relation.ts +++ b/src/model/attributes/relations/Relation.ts @@ -5,7 +5,7 @@ import Query from '../../../query/Query' import Model from '../../Model' import Attribute from '../Attribute' -export interface Dictionaly { +export interface Dictionary { [id: string]: Model[] } @@ -47,12 +47,12 @@ export default abstract class Relation extends Attribute { abstract attach(ids: any, record: Record, data: NormalizedData): void /** - * Set the constraints for an eager load of the relation. + * Set the constraints for an eager loading relation. */ abstract addEagerConstraints(query: Query, models: Collection): void /** - * Get the relationship for eager loading. + * Execute the eager loading query. */ public getEager(query: Query): Collection { return query.get() @@ -80,17 +80,17 @@ export default abstract class Relation extends Attribute { protected mapToDictionary( models: Collection, callback: (model: Model) => [string, Model] - ): Dictionaly { - return models.reduce((dictionaly, model) => { + ): Dictionary { + return models.reduce((dictionary, model) => { const [key, value] = callback(model) - if (!dictionaly[key]) { - dictionaly[key] = [] + if (!dictionary[key]) { + dictionary[key] = [] } - dictionaly[key].push(value) + dictionary[key].push(value) - return dictionaly + return dictionary }, {}) } } From 06f0c5aa7ad07606bed2406f8a39aaa70dc9a62b Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 16:33:16 +0900 Subject: [PATCH 49/56] chore: rename query tests to repository tests --- test/feature/{query => repository}/deletes_delete.spec.ts | 2 +- test/feature/{query => repository}/deletes_delete_all.spec.ts | 2 +- test/feature/{query => repository}/deletes_destroy.spec.ts | 2 +- test/feature/{query => repository}/inserts_insert.spec.ts | 2 +- test/feature/{query => repository}/retrieves_all.spec.ts | 2 +- test/feature/{query => repository}/retrieves_find.spec.ts | 2 +- test/feature/{query => repository}/retrieves_where.spec.ts | 2 +- test/feature/{query => repository}/retrieves_where_or.spec.ts | 2 +- test/feature/{query => repository}/updates_update.spec.ts | 2 +- test/feature/{query => repository}/updates_update_query.spec.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename test/feature/{query => repository}/deletes_delete.spec.ts (95%) rename test/feature/{query => repository}/deletes_delete_all.spec.ts (91%) rename test/feature/{query => repository}/deletes_destroy.spec.ts (96%) rename test/feature/{query => repository}/inserts_insert.spec.ts (95%) rename test/feature/{query => repository}/retrieves_all.spec.ts (93%) rename test/feature/{query => repository}/retrieves_find.spec.ts (94%) rename test/feature/{query => repository}/retrieves_where.spec.ts (96%) rename test/feature/{query => repository}/retrieves_where_or.spec.ts (96%) rename test/feature/{query => repository}/updates_update.spec.ts (96%) rename test/feature/{query => repository}/updates_update_query.spec.ts (96%) diff --git a/test/feature/query/deletes_delete.spec.ts b/test/feature/repository/deletes_delete.spec.ts similarity index 95% rename from test/feature/query/deletes_delete.spec.ts rename to test/feature/repository/deletes_delete.spec.ts index f29de5a0..924b3724 100644 --- a/test/feature/query/deletes_delete.spec.ts +++ b/test/feature/repository/deletes_delete.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState, assertState } from 'test/Helpers' import { Model, Attr, Str } from '@/index' -describe('feature/query/deletes_delete', () => { +describe('feature/repository/deletes_delete', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/deletes_delete_all.spec.ts b/test/feature/repository/deletes_delete_all.spec.ts similarity index 91% rename from test/feature/query/deletes_delete_all.spec.ts rename to test/feature/repository/deletes_delete_all.spec.ts index 15cb7822..9a13928c 100644 --- a/test/feature/query/deletes_delete_all.spec.ts +++ b/test/feature/repository/deletes_delete_all.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState, assertState } from 'test/Helpers' import { Model, Attr, Str } from '@/index' -describe('feature/query/deletes_delete_all', () => { +describe('feature/repository/deletes_delete_all', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/deletes_destroy.spec.ts b/test/feature/repository/deletes_destroy.spec.ts similarity index 96% rename from test/feature/query/deletes_destroy.spec.ts rename to test/feature/repository/deletes_destroy.spec.ts index 4ea373ce..552cb478 100644 --- a/test/feature/query/deletes_destroy.spec.ts +++ b/test/feature/repository/deletes_destroy.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState, assertState } from 'test/Helpers' import { Model, Attr, Str } from '@/index' -describe('feature/query/deletes_destroy', () => { +describe('feature/repository/deletes_destroy', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/inserts_insert.spec.ts b/test/feature/repository/inserts_insert.spec.ts similarity index 95% rename from test/feature/query/inserts_insert.spec.ts rename to test/feature/repository/inserts_insert.spec.ts index e2d7c0e5..27b4afcb 100644 --- a/test/feature/query/inserts_insert.spec.ts +++ b/test/feature/repository/inserts_insert.spec.ts @@ -1,7 +1,7 @@ import { createStore, assertState } from 'test/Helpers' import { Model, Attr, Str } from '@/index' -describe('feature/query/inserts_insert', () => { +describe('feature/repository/inserts_insert', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/retrieves_all.spec.ts b/test/feature/repository/retrieves_all.spec.ts similarity index 93% rename from test/feature/query/retrieves_all.spec.ts rename to test/feature/repository/retrieves_all.spec.ts index d961b6ea..11b0b331 100644 --- a/test/feature/query/retrieves_all.spec.ts +++ b/test/feature/repository/retrieves_all.spec.ts @@ -6,7 +6,7 @@ import { } from 'test/Helpers' import { Model, Attr, Str } from '@/index' -describe('feature/query/retrieves_all', () => { +describe('feature/repository/retrieves_all', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/retrieves_find.spec.ts b/test/feature/repository/retrieves_find.spec.ts similarity index 94% rename from test/feature/query/retrieves_find.spec.ts rename to test/feature/repository/retrieves_find.spec.ts index 8940d6dd..e691e23d 100644 --- a/test/feature/query/retrieves_find.spec.ts +++ b/test/feature/repository/retrieves_find.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState } from 'test/Helpers' import { Model, Attr, Str } from '@/index' -describe('feature/query/retrieves_find', () => { +describe('feature/repository/retrieves_find', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/retrieves_where.spec.ts b/test/feature/repository/retrieves_where.spec.ts similarity index 96% rename from test/feature/query/retrieves_where.spec.ts rename to test/feature/repository/retrieves_where.spec.ts index 24d8a92c..2c941167 100644 --- a/test/feature/query/retrieves_where.spec.ts +++ b/test/feature/repository/retrieves_where.spec.ts @@ -6,7 +6,7 @@ import { } from 'test/Helpers' import { Model, Attr, Str, Num } from '@/index' -describe('feature/query/retrieves_where', () => { +describe('feature/repository/retrieves_where', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/retrieves_where_or.spec.ts b/test/feature/repository/retrieves_where_or.spec.ts similarity index 96% rename from test/feature/query/retrieves_where_or.spec.ts rename to test/feature/repository/retrieves_where_or.spec.ts index f8308554..68bb6287 100644 --- a/test/feature/query/retrieves_where_or.spec.ts +++ b/test/feature/repository/retrieves_where_or.spec.ts @@ -6,7 +6,7 @@ import { } from 'test/Helpers' import { Model, Attr, Str, Num } from '@/index' -describe('feature/query/retrieves_where', () => { +describe('feature/repository/retrieves_where', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/updates_update.spec.ts b/test/feature/repository/updates_update.spec.ts similarity index 96% rename from test/feature/query/updates_update.spec.ts rename to test/feature/repository/updates_update.spec.ts index 5332080f..09ff3b3e 100644 --- a/test/feature/query/updates_update.spec.ts +++ b/test/feature/repository/updates_update.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState, assertState } from 'test/Helpers' import { Model, Attr, Str, Num } from '@/index' -describe('feature/query/updates_update', () => { +describe('feature/repository/updates_update', () => { class User extends Model { static entity = 'users' diff --git a/test/feature/query/updates_update_query.spec.ts b/test/feature/repository/updates_update_query.spec.ts similarity index 96% rename from test/feature/query/updates_update_query.spec.ts rename to test/feature/repository/updates_update_query.spec.ts index 0dab56ce..7655449f 100644 --- a/test/feature/query/updates_update_query.spec.ts +++ b/test/feature/repository/updates_update_query.spec.ts @@ -1,7 +1,7 @@ import { createStore, fillState, assertState } from 'test/Helpers' import { Model, Attr, Str, Num } from '@/index' -describe('feature/query/updates_update_query', () => { +describe('feature/repository/updates_update_query', () => { class User extends Model { static entity = 'users' From c6c77301e5a17d865d147e57c66b768e64895d87 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 17:31:33 +0900 Subject: [PATCH 50/56] feat: make `destroy` method accept an array of ids --- src/model/Model.ts | 13 +++++ src/query/Query.ts | 55 +++++++++---------- src/repository/Repository.ts | 19 +++---- .../repository/deletes_destroy.spec.ts | 2 +- 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/model/Model.ts b/src/model/Model.ts index 1b848b8b..08a2a9a5 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -244,6 +244,9 @@ export default class Model { return this } + /** + * Fill type attribute filed. + */ protected $fillTypeField( key: string, attr: Attributes.Attribute, @@ -262,6 +265,9 @@ export default class Model { this[key] = attr.make() } + /** + * Fill relation attribute filed. + */ protected $fillRelationFields( key: string, attr: Attributes.Relation, @@ -272,6 +278,13 @@ export default class Model { } } + /** + * Get the primary key field name. + */ + $getPrimaryKey(): string { + return this.$primaryKey + } + /** * Get the index id for the model or the given record. */ diff --git a/src/query/Query.ts b/src/query/Query.ts index 8be85a5a..d8882117 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -77,6 +77,13 @@ export default class Query { return this } + /** + * Add a where clause on the primary key to the query. + */ + whereId(ids: string | number | (string | number)[]): this { + return this.where(this.model.$getPrimaryKey(), ids) + } + /** * Add an "or where" clause to the query. */ @@ -333,53 +340,41 @@ export default class Query { } /** - * Delete records that match the query chain. + * Destroy the models for the given id. */ - async delete(): Promise> { - const models = this.get() - - this.connection().delete(this.getIdsFromCollection(models)) + async destroy(id: string | number): Promise> + async destroy(ids: Array): Promise> + async destroy(ids: any): Promise { + const models = await this.destroyMany(isArray(ids) ? ids : [ids]) - return models + return models[0] ?? null } /** - * Delete all records in the store. + * Destroy the models for the given id. */ - async deleteAll(): Promise> { - const models = this.getModels() - - this.connection().deleteAll() - - return models + async destroyMany(ids: (string | number)[]): Promise> { + return this.whereId(ids).delete() } /** - * Destroy the models for the given id. + * Delete records that match the query chain. */ - async destroy(id: string | number): Promise> { - const indexId = this.normalizeIndexId(id) - - const model = this.find(indexId) - - if (!model) { - return null - } + async delete(): Promise> { + const models = this.get() - this.connection().delete([indexId]) + this.connection().delete(this.getIdsFromCollection(models)) - return model + return models } /** - * Destroy the models for the given id. + * Delete all records in the store. */ - async destroyMany(ids: (string | number)[]): Promise> { - const indexIds = this.normalizeIndexIds(ids) - - const models = this.findIn(indexIds) + async deleteAll(): Promise> { + const models = this.getModels() - this.connection().delete(indexIds) + this.connection().deleteAll() return models } diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index d39bf58b..8c5ef6dc 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -189,24 +189,19 @@ export default class Repository { return this.interpretation().process(records) } - /** - * Delete all records in the store. - */ - deleteAll(): Promise> { - return this.query().deleteAll() - } - /** * Destroy the models for the given id. */ - destroy(id: string | number): Promise> { - return this.query().destroy(id) + destroy(id: string | number): Promise> + destroy(ids: (string | number)[]): Promise> + destroy(ids: any): Promise { + return this.query().destroy(ids) } /** - * Destroy the models for the given ids. + * Delete all records in the store. */ - destroyMany(ids: (string | number)[]): Promise> { - return this.query().destroyMany(ids) + deleteAll(): Promise> { + return this.query().deleteAll() } } diff --git a/test/feature/repository/deletes_destroy.spec.ts b/test/feature/repository/deletes_destroy.spec.ts index 552cb478..b6f51744 100644 --- a/test/feature/repository/deletes_destroy.spec.ts +++ b/test/feature/repository/deletes_destroy.spec.ts @@ -61,7 +61,7 @@ describe('feature/repository/deletes_destroy', () => { } }) - await store.$repo(User).destroyMany([2, 3]) + await store.$repo(User).destroy([2, 3]) assertState(store, { users: { From be9fe2129c0cf89d3983deb9eb30bd65866bfe0c Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 17:53:39 +0900 Subject: [PATCH 51/56] refactor: improve test coverage --- src/connection/Connection.ts | 2 +- src/query/Query.ts | 32 ++++++++++++------- src/repository/Repository.ts | 6 ++-- .../feature/repository/retrieves_find.spec.ts | 25 ++++++++++++++- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index 4eaf2def..ff8b189d 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -64,7 +64,7 @@ export default class Connection { /** * Find a model by its primary key. */ - find(id: string | number): Record | null { + find(id: string): Record | null { return this.getData()[id] ?? null } diff --git a/src/query/Query.ts b/src/query/Query.ts index d8882117..49b957ee 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -143,10 +143,16 @@ export default class Query { /** * Find a model by its primary key. */ - find(id: string | number): Item { - const record = this.findRaw(id) + find(id: string | number): Item + find(ids: (string | number)[]): Collection + find(ids: any): any { + if (isArray(ids)) { + return this.findIn(ids) + } + + const model = this.findRaw(ids) - return record ? this.hydrateRecord(record) : null + return model ? this.hydrateRecord(model) : null } /** @@ -345,9 +351,11 @@ export default class Query { async destroy(id: string | number): Promise> async destroy(ids: Array): Promise> async destroy(ids: any): Promise { - const models = await this.destroyMany(isArray(ids) ? ids : [ids]) + if (isArray(ids)) { + return this.destroyMany(ids) + } - return models[0] ?? null + return (await this.whereId(ids).delete())[0] ?? null } /** @@ -379,13 +387,6 @@ export default class Query { return models } - /** - * Get an array of ids from the given collection. - */ - protected getIdsFromCollection(models: Collection): string[] { - return models.map((model) => model.$getIndexId()) - } - /** * Normalize the given index id. This method will convert the given key to * the string since the index key must be a string. @@ -402,6 +403,13 @@ export default class Query { return ids.map((id) => String(id)) } + /** + * Get an array of ids from the given collection. + */ + protected getIdsFromCollection(models: Collection): string[] { + return models.map((model) => model.$getIndexId()) + } + /** * Instantiate new models with the given record. */ diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 8c5ef6dc..843f23f8 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -85,8 +85,10 @@ export default class Repository { /** * Find the model with the given id. */ - find(id: string | number): Data.Item { - return this.query().find(id) + find(id: string | number): Data.Item + find(ids: (string | number)[]): Data.Collection + find(ids: any): Data.Item { + return this.query().find(ids) } /** diff --git a/test/feature/repository/retrieves_find.spec.ts b/test/feature/repository/retrieves_find.spec.ts index e691e23d..90a32c0b 100644 --- a/test/feature/repository/retrieves_find.spec.ts +++ b/test/feature/repository/retrieves_find.spec.ts @@ -1,4 +1,4 @@ -import { createStore, fillState } from 'test/Helpers' +import { createStore, fillState, assertInstanceOf, assertModels } from 'test/Helpers' import { Model, Attr, Str } from '@/index' describe('feature/repository/retrieves_find', () => { @@ -43,4 +43,27 @@ describe('feature/repository/retrieves_find', () => { expect(user).toBe(null) }) + + it('can find records by ids', () => { + const store = createStore([User]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + 3: { id: 3, name: 'Johnny Doe' } + } + }) + + const users = store.$repo(User).find([1, 3]) + + const expected = [ + { id: 1, name: 'John Doe' }, + { id: 3, name: 'Johnny Doe' } + ] + + expect(users.length).toBe(2) + assertInstanceOf(users, User) + assertModels(users, expected) + }) }) From 0f5af2e22339f3dbaf5c971c8e97e096a429491e Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 17:55:30 +0900 Subject: [PATCH 52/56] style: fix linter issue --- test/feature/repository/retrieves_find.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/feature/repository/retrieves_find.spec.ts b/test/feature/repository/retrieves_find.spec.ts index 90a32c0b..5249ce42 100644 --- a/test/feature/repository/retrieves_find.spec.ts +++ b/test/feature/repository/retrieves_find.spec.ts @@ -1,4 +1,9 @@ -import { createStore, fillState, assertInstanceOf, assertModels } from 'test/Helpers' +import { + createStore, + fillState, + assertInstanceOf, + assertModels +} from 'test/Helpers' import { Model, Attr, Str } from '@/index' describe('feature/repository/retrieves_find', () => { From 8729f2d8ff5df1387325fd26dbaca6e0d8efa079 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 20:51:31 +0900 Subject: [PATCH 53/56] feat: add has many relation --- src/index.ts | 2 + src/model/Model.ts | 60 ++++++++----- src/model/attributes/Attributes.ts | 13 ++- src/model/attributes/relations/HasMany.ts | 90 +++++++++++++++++++ src/model/attributes/relations/HasOne.ts | 3 - .../attributes/relations/HasMany.ts | 17 ++++ src/query/Query.ts | 2 +- .../relations/types/has_many_insert.spec.ts | 84 +++++++++++++++++ .../relations/types/has_many_retrieve.spec.ts | 80 +++++++++++++++++ 9 files changed, 322 insertions(+), 29 deletions(-) create mode 100644 src/model/attributes/relations/HasMany.ts create mode 100644 src/model/decorators/attributes/relations/HasMany.ts create mode 100644 test/feature/relations/types/has_many_insert.spec.ts create mode 100644 test/feature/relations/types/has_many_retrieve.spec.ts diff --git a/src/index.ts b/src/index.ts index a466ecc1..8c9edc98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import Str from './model/decorators/attributes/types/Str' import Num from './model/decorators/attributes/types/Num' import Bool from './model/decorators/attributes/types/Bool' import HasOne from './model/decorators/attributes/relations/HasOne' +import HasMany from './model/decorators/attributes/relations/HasMany' import RootModule from './modules/RootModule' import RootState from './modules/RootState' import Module from './modules/Module' @@ -36,6 +37,7 @@ export { Num, Bool, HasOne, + HasMany, RootModule, RootState, Module, diff --git a/src/model/Model.ts b/src/model/Model.ts index 08a2a9a5..f1f61640 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,5 +1,4 @@ import { Store } from 'vuex' -import { isObject } from '../support/Utils' import { Record } from '../data/Data' import * as Attributes from './attributes/Attributes' @@ -19,6 +18,10 @@ interface Registry { [key: string]: () => Attributes.Attribute } +interface FillOptions { + relations?: boolean +} + export default class Model { /** * The store instance. @@ -56,10 +59,10 @@ export default class Model { /** * Create a new model instance. */ - constructor(attributes?: Record) { + constructor(attributes?: Record, options?: FillOptions) { this.$boot() - this.$fill(attributes) + this.$fill(attributes, options) } /** @@ -145,6 +148,24 @@ export default class Model { ) } + /** + * Create a new has many relation instance. + */ + static hasMany( + related: typeof Model, + foreignKey: string, + localKey?: string + ): Attributes.HasMany { + localKey = localKey ?? this.getLocalKey() + + return new Attributes.HasMany( + new this(), + new related(), + foreignKey, + localKey + ) + } + /** * Get the local key for the model. */ @@ -194,8 +215,8 @@ export default class Model { * way for us to generate fresh model instances of this current model. It's * particularly useful during the hydration of new objects via the query. */ - $newInstance(attributes: Record): this { - const model = new this.$self(attributes) as this + $newInstance(attributes?: Record, options?: FillOptions): this { + const model = new this.$self(attributes, options) as this model.$setStore(model.$store) @@ -231,23 +252,27 @@ export default class Model { * Fill the model by the given attributes. Its default values will fill any * missing field. */ - $fill(attributes: Record = {}): this { + $fill(attributes: Record = {}, options: FillOptions = {}): this { + const fillRelation = options.relations ?? true + for (const key in this.$fields) { const attr = this.$fields[key] const value = attributes[key] - attr instanceof Attributes.Relation - ? this.$fillRelationFields(key, attr, value) - : this.$fillTypeField(key, attr, value) + if (attr instanceof Attributes.Relation && !fillRelation) { + continue + } + + this.$fillField(key, attr, value) } return this } /** - * Fill type attribute filed. + * Fill the model filed. */ - protected $fillTypeField( + protected $fillField( key: string, attr: Attributes.Attribute, value: any @@ -265,19 +290,6 @@ export default class Model { this[key] = attr.make() } - /** - * Fill relation attribute filed. - */ - protected $fillRelationFields( - key: string, - attr: Attributes.Relation, - value: any - ): void { - if (isObject(value)) { - this[key] = attr.make(value) - } - } - /** * Get the primary key field name. */ diff --git a/src/model/attributes/Attributes.ts b/src/model/attributes/Attributes.ts index 2393489c..ecdff443 100644 --- a/src/model/attributes/Attributes.ts +++ b/src/model/attributes/Attributes.ts @@ -6,5 +6,16 @@ import Number from './types/Number' import Boolean from './types/Boolean' import Relation from './relations/Relation' import HasOne from './relations/HasOne' +import HasMany from './relations/HasMany' -export { Attribute, Type, Attr, String, Number, Boolean, Relation, HasOne } +export { + Attribute, + Type, + Attr, + String, + Number, + Boolean, + Relation, + HasOne, + HasMany +} diff --git a/src/model/attributes/relations/HasMany.ts b/src/model/attributes/relations/HasMany.ts new file mode 100644 index 00000000..a02109a8 --- /dev/null +++ b/src/model/attributes/relations/HasMany.ts @@ -0,0 +1,90 @@ +import { Schema as NormalizrSchema } from 'normalizr' +import Schema from '../../../schema/Schema' +import { Record, NormalizedData, Collection } from '../../../data/Data' +import Query from '../../../query/Query' +import Model from '../../Model' +import Relation, { Dictionary } from './Relation' + +export default class HasMany extends Relation { + /** + * The foreign key of the parent model. + */ + protected foreignKey: string + + /** + * The local key of the parent model. + */ + protected localKey: string + + /** + * Create a new has many relation instance. + */ + constructor( + parent: Model, + related: Model, + foreignKey: string, + localKey: string + ) { + super(parent, related) + this.foreignKey = foreignKey + this.localKey = localKey + } + + /** + * Define the normalizr schema for the relationship. + */ + define(schema: Schema): NormalizrSchema { + return schema.many(this.related) + } + + /** + * Attach the relational key to the given data. + */ + attach(ids: (string | number)[], record: Record, data: NormalizedData): void { + ids.forEach((id) => { + const relatedRecord = data[this.related.$entity]?.[id] + + if (relatedRecord) { + relatedRecord[this.foreignKey] = record[this.localKey] + } + }) + } + + /** + * Set the constraints for an eager load of the relation. + */ + addEagerConstraints(query: Query, models: Collection): void { + query.whereIn(this.foreignKey, this.getKeys(models, this.localKey)) + } + + /** + * Match the eagerly loaded results to their parents. + */ + match(relation: string, models: Collection, results: Collection): void { + const dictionary = this.buildDictionary(results) + + models.map((model) => { + const key = model[this.localKey] + + dictionary[key] + ? model.$setRelation(relation, dictionary[key]) + : model.$setRelation(relation, []) + }) + } + + /** + * Build model dictionary keyed by the relation's foreign key. + */ + protected buildDictionary(results: Collection): Dictionary { + return this.mapToDictionary(results, (result) => { + return [result[this.foreignKey], result] + }) + } + + /** + * Make the value for the attribute. + */ + make(_value: any): null { + return null + } +} diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index c79a868d..412c889a 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -72,9 +72,6 @@ export default class HasOne extends Relation { /** * Build model dictionary keyed by the relation's foreign key. - * - * @param \Illuminate\Database\Eloquent\Collection $results - * @return array */ protected buildDictionary(results: Collection): Dictionary { return this.mapToDictionary(results, (result) => { diff --git a/src/model/decorators/attributes/relations/HasMany.ts b/src/model/decorators/attributes/relations/HasMany.ts new file mode 100644 index 00000000..850ff621 --- /dev/null +++ b/src/model/decorators/attributes/relations/HasMany.ts @@ -0,0 +1,17 @@ +import Model from '../../../Model' +import { PropertyDecorator } from '../../Contracts' + +/** + * Create a has many decorator. + */ +export default function HasMany( + related: () => typeof Model, + foreignKey: string, + localKey?: string +): PropertyDecorator { + return (target, propertyKey) => { + target.$self.setRegistry(propertyKey, () => + target.$self.hasMany(related(), foreignKey, localKey) + ) + } +} diff --git a/src/query/Query.ts b/src/query/Query.ts index 49b957ee..a9ba8693 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -414,7 +414,7 @@ export default class Query { * Instantiate new models with the given record. */ protected hydrateRecord(record: Record): M { - return this.model.$newInstance(record) + return this.model.$newInstance(record, { relations: false }) } /** diff --git a/test/feature/relations/types/has_many_insert.spec.ts b/test/feature/relations/types/has_many_insert.spec.ts new file mode 100644 index 00000000..25060218 --- /dev/null +++ b/test/feature/relations/types/has_many_insert.spec.ts @@ -0,0 +1,84 @@ +import { createStore, assertState } from 'test/Helpers' +import { Model, Attr, Str, HasMany } from '@/index' + +describe('feature/relations/types/has_many_insert', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + + @HasMany(() => Post, 'userId') + posts!: Post[] + } + + class Post extends Model { + static entity = 'posts' + + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string + } + + it('inserts a record to the store with "has many" relation', async () => { + const store = createStore([User, Post]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe', + posts: [ + { id: 1, userId: 1, title: 'Title 01' }, + { id: 2, userId: 1, title: 'Title 02' } + ] + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' }, + 2: { id: 2, userId: 1, title: 'Title 02' } + } + }) + }) + + it('generates missing foreign key', async () => { + const store = createStore([User, Post]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe', + posts: [ + { id: 1, title: 'Title 01' }, + { id: 2, title: 'Title 02' } + ] + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' }, + 2: { id: 2, userId: 1, title: 'Title 02' } + } + }) + }) + + it('can insert a record with missing relational key', async () => { + const store = createStore([User, Post]) + + await store.$repo(User).insert({ + id: 1, + name: 'John Doe' + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: {} + }) + }) +}) diff --git a/test/feature/relations/types/has_many_retrieve.spec.ts b/test/feature/relations/types/has_many_retrieve.spec.ts new file mode 100644 index 00000000..04f3be51 --- /dev/null +++ b/test/feature/relations/types/has_many_retrieve.spec.ts @@ -0,0 +1,80 @@ +import { + createStore, + fillState, + assertInstanceOf, + assertModel +} from 'test/Helpers' +import { Model, Attr, Str, HasMany } from '@/index' + +describe('feature/relations/types/has_many_retrieve', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + + @HasMany(() => Post, 'userId') + posts!: Post[] + } + + class Post extends Model { + static entity = 'posts' + + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string + } + + it('can eager load has one relation', async () => { + const store = createStore([User, Post]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' }, + 2: { id: 2, userId: 1, title: 'Title 02' } + } + }) + + const user = store + .$repo(User) + .with('posts') + .first()! + + expect(user).toBeInstanceOf(User) + assertInstanceOf(user.posts, Post) + assertModel(user, { + id: 1, + name: 'John Doe', + posts: [ + { id: 1, userId: 1, title: 'Title 01' }, + { id: 2, userId: 1, title: 'Title 02' } + ] + }) + }) + + it('can eager load missing relation as empty array', async () => { + const store = createStore([User, Post]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: {} + }) + + const user = store + .$repo(User) + .with('posts') + .first()! + + expect(user).toBeInstanceOf(User) + assertModel(user, { + id: 1, + name: 'John Doe', + posts: [] + }) + }) +}) From 87f72b22d1d4f4e412cb8ad23186098d78164d63 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 23:44:25 +0900 Subject: [PATCH 54/56] build: add api-extractor support --- .gitignore | 1 + api-extractor.json | 48 + package.json | 27 +- scripts/build.js | 7 + src/connection/Connection.ts | 33 +- src/data/Data.ts | 16 +- src/database/Database.ts | 34 +- src/index.cjs.ts | 40 +- src/index.ts | 108 +- src/interpretation/Interpretation.ts | 24 +- src/model/Model.ts | 111 +- src/model/attributes/Attribute.ts | 4 +- src/model/attributes/Attributes.ts | 21 - src/model/attributes/relations/HasMany.ts | 24 +- src/model/attributes/relations/HasOne.ts | 20 +- src/model/attributes/relations/Relation.ts | 14 +- src/model/attributes/types/Attr.ts | 4 +- src/model/attributes/types/Boolean.ts | 6 +- src/model/attributes/types/Number.ts | 6 +- src/model/attributes/types/String.ts | 6 +- src/model/attributes/types/Type.ts | 6 +- src/model/decorators/Contracts.ts | 2 +- .../attributes/relations/HasMany.ts | 4 +- .../decorators/attributes/relations/HasOne.ts | 4 +- src/model/decorators/attributes/types/Attr.ts | 2 +- src/model/decorators/attributes/types/Bool.ts | 2 +- src/model/decorators/attributes/types/Num.ts | 2 +- src/model/decorators/attributes/types/Str.ts | 2 +- src/modules/Module.ts | 6 +- src/modules/Mutations.ts | 16 +- src/modules/RootModule.ts | 5 +- src/modules/RootState.ts | 4 +- src/modules/State.ts | 6 +- src/query/{options => }/Options.ts | 2 +- src/query/Query.ts | 58 +- src/repository/Repository.ts | 75 +- src/schema/Schema.ts | 12 +- src/store/Store.ts | 23 +- src/types/vuex.d.ts | 6 +- test/Helpers.ts | 14 +- .../relations/types/has_many_retrieve.spec.ts | 10 +- .../relations/types/has_one_retrieve.spec.ts | 10 +- .../feature/repository/deletes_delete.spec.ts | 5 +- .../repository/retrieves_where.spec.ts | 5 +- .../repository/retrieves_where_or.spec.ts | 5 +- .../repository/updates_update_query.spec.ts | 5 +- test/unit/model/Model_Attrs_Boolean.spec.ts | 4 +- test/unit/model/Model_Attrs_Number.spec.ts | 4 +- test/unit/model/Model_Attrs_String.spec.ts | 4 +- tsconfig.json | 3 +- yarn.lock | 1040 ++++++++++------- 51 files changed, 1031 insertions(+), 869 deletions(-) create mode 100644 api-extractor.json delete mode 100644 src/model/attributes/Attributes.ts rename src/query/{options => }/Options.ts (89%) diff --git a/.gitignore b/.gitignore index eea953e6..04e6c182 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /docs/.vuepress/dist /docs/node_modules /node_modules +/temp .idea/* .vscode/* yarn-error.log diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 00000000..71780452 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "./dist/src/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "/temp/" + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "./dist/index.d.ts" + }, + + "tsdocMetadata": { + "enabled": false + }, + + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + + "extractorMessageReporting": { + "default": { + "logLevel": "warning", + "addToApiReportFile": true + }, + + "ae-missing-release-tag": { + "logLevel": "none" + } + }, + + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + } + } + } +} diff --git a/package.json b/package.json index a2e47def..b75fdc53 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,14 @@ "browser": "dist/vuex-orm.esm.js", "module": "dist/vuex-orm.esm-bundler.js", "unpkg": "dist/vuex-orm.global.js", - "typings": "dist/src/index.d.ts", + "typings": "dist/index.d.ts", "files": [ "dist" ], "scripts": { "build": "node scripts/build.js", - "clean": "rm -rf dist && rm -rf dist && rm -rf coverage && rm -rf .nyc_output && rm -rf .tmp", + "build:dts": "api-extractor run --local --verbose", + "clean": "rm -rf dist && rm -rf dist && rm -rf coverage && && rm -rf temp && rm -rf .nyc_output && rm -rf .tmp", "lint": "prettier --check --write --parser typescript \"{src,test}/**/*.ts\"", "lint:fail": "prettier --check --parser typescript \"{src,test}/**/*.ts\"", "test": "jest --testPathIgnorePatterns test/performance", @@ -22,7 +23,9 @@ "test:all": "jest", "coverage": "jest --testPathIgnorePatterns test/performance --collect-coverage", "docs": "vuepress dev docs", - "docs:build": "vuepress build docs", + "docs:build": "yarn docs:build:guide && yarn docs:build:api", + "docs:build:guide": "vuepress build docs", + "docs:build:api": "yarn build && api-documenter markdown --input-folder temp --output-folder docs-api", "docs:deploy": "bash ./docs/deploy.sh", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "release": "node scripts/release.js" @@ -50,24 +53,26 @@ "normalizr": "^3.6.0" }, "devDependencies": { + "@microsoft/api-documenter": "^7.7.18", + "@microsoft/api-extractor": "^7.7.13", "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@rollup/plugin-replace": "^2.3.1", - "@types/jest": "^25.1.4", + "@types/jest": "^25.2.1", "brotli": "^1.3.2", - "chalk": "^3.0.0", + "chalk": "^4.0.0", "codecov": "^3.6.4", "conventional-changelog-cli": "^2.0.31", "core-js": "^3.6.4", - "enquirer": "^2.3.4", + "enquirer": "^2.3.5", "execa": "^4.0.0", - "jest": "^25.2.4", - "prettier": "1.19.1", - "rollup": "^2.3.2", + "jest": "^25.3.0", + "prettier": "^2.0.4", + "rollup": "^2.3.4", "rollup-plugin-terser": "^5.3.0", "rollup-plugin-typescript2": "^0.27.0", - "semver": "^7.1.3", - "ts-jest": "^25.3.0", + "semver": "^7.2.1", + "ts-jest": "^25.3.1", "typescript": "^3.8.3", "vue": "^2.6.11", "vue-server-renderer": "^2.6.11", diff --git a/scripts/build.js b/scripts/build.js index 1b4053ff..c7b67cbe 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -16,6 +16,7 @@ const files = [ async function run() { await build() checkAllSizes() + generateApiDocs() } async function build() { @@ -44,4 +45,10 @@ function checkSize(file) { ) } +async function generateApiDocs() { + await execa('yarn', ['build:dts'], { stdio: 'inherit' }) + + await fs.remove('dist/src') +} + run() diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index ff8b189d..3759f9a2 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -1,13 +1,8 @@ import { Store } from 'vuex' -import { Record, Records } from '../data/Data' -import Model from '../model/Model' +import { Element, Elements } from '../data/Data' +import { Model } from '../model/Model' -interface Namespace { - connection: string - entity: string -} - -export default class Connection { +export class Connection { /** * The store instance. */ @@ -29,7 +24,7 @@ export default class Connection { /** * Get namespace for the modul */ - private getNamespace(): Namespace { + private getNamespace(): { connection: string; entity: string } { const connection = this.store.$database.connection const entity = this.model.$entity @@ -39,7 +34,7 @@ export default class Connection { /** * Get the state object from the store. */ - private getData(): Records { + private getData(): Elements { const { connection, entity } = this.getNamespace() return this.store.state[connection][entity].data @@ -57,21 +52,21 @@ export default class Connection { /** * Get all existing records. */ - get(): Records { + get(): Elements { return this.getData() } /** * Find a model by its primary key. */ - find(id: string): Record | null { + find(id: string): Element | null { return this.getData()[id] ?? null } /** * Find multiple models by their primary keys. */ - findIn(ids: string[]): Record[] { + findIn(ids: string[]): Element[] { const data = this.getData() return ids.map((id) => data[id]) @@ -80,22 +75,22 @@ export default class Connection { /** * Commit `insert` change to the store. */ - insert(records: Record[]): void { - this.commit('insert', this.mapRecords(records)) + insert(records: Element[]): void { + this.commit('insert', this.mapElements(records)) } /** * Commit `update` change to the store. */ - update(records: Record[]): void { - this.commit('update', this.mapRecords(records)) + update(records: Element[]): void { + this.commit('update', this.mapElements(records)) } /** * Convert the given array of records into records. */ - private mapRecords(records: Record[]): Records { - return records.reduce((carry, record) => { + private mapElements(records: Element[]): Elements { + return records.reduce((carry, record) => { carry[this.model.$getIndexId(record)] = record return carry }, {}) diff --git a/src/data/Data.ts b/src/data/Data.ts index 781fa0c6..115557a3 100644 --- a/src/data/Data.ts +++ b/src/data/Data.ts @@ -1,21 +1,17 @@ -import Model from '../model/Model' +import { Model } from '../model/Model' -export interface Record { - [key: string]: any -} +export type Element = Record -export interface Records { - [id: string]: Record +export interface Elements { + [id: string]: Element } export interface NormalizedData { - [entity: string]: Records + [entity: string]: Elements } export type Item = M | null export type Collection = M[] -export interface Collections { - [name: string]: Collection -} +export type Collections = Record> diff --git a/src/database/Database.ts b/src/database/Database.ts index ff8a1504..47acf947 100644 --- a/src/database/Database.ts +++ b/src/database/Database.ts @@ -1,22 +1,14 @@ -import { Store, Module as VuexModule, MutationTree } from 'vuex' +import { Store, Module as VuexModule } from 'vuex' import { schema as Normalizr } from 'normalizr' import { Constructor } from '../types' -import Schema from '../schema/Schema' -import Model from '../model/Model' -import RootModule from '../modules/RootModule' -import Module from '../modules/Module' -import State from '../modules/State' -import Mutations from '../modules/Mutations' - -interface Models { - [name: string]: Model -} - -interface Schemas { - [name: string]: Normalizr.Entity -} +import { Schema } from '../schema/Schema' +import { Model } from '../model/Model' +import { RootModule } from '../modules/RootModule' +import { Module } from '../modules/Module' +import { State } from '../modules/State' +import { mutations, Mutations } from '../modules/Mutations' -export default class Database { +export class Database { /** * The store instance. */ @@ -31,12 +23,12 @@ export default class Database { /** * The list of registered models. */ - models: Models = {} + models: Record = {} /** * The schema definition for the registered models. */ - schemas: Schemas = {} + schemas: Record = {} /** * Whether the database has already been installed to Vuex or not. @@ -88,7 +80,7 @@ export default class Database { * Get the model. */ getModel(name: string): M { - return this.models[name] as M + return this.models[name] as any } /** @@ -178,7 +170,7 @@ export default class Database { /** * Create sub mutations. */ - private createSubMutations(): MutationTree { - return Mutations + private createSubMutations(): Mutations { + return mutations } } diff --git a/src/index.cjs.ts b/src/index.cjs.ts index 145776bb..a135128a 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,27 +1,41 @@ import './polyfills/Polyfills' import { install } from './store/Store' -import Database from './database/Database' -import Schema from './schema/Schema' -import Model from './model/Model' -import * as Attributes from './model/attributes/Attributes' -import Mutations from './modules/Mutations' -import Repository from './repository/Repository' -import Interpretation from './interpretation/Interpretation' -import Query from './query/Query' -import * as QueryOptions from './query/options/Options' -import Connection from './connection/Connection' +import { Database } from './database/Database' +import { Schema } from './schema/Schema' +import { Model } from './model/Model' +import { Attribute } from './model/attributes/Attribute' +import { Type } from './model/attributes/types/Type' +import { Attr as AttrAttr } from './model/attributes/types/Attr' +import { String as StringAttr } from './model/attributes/types/String' +import { Number as NumberAttr } from './model/attributes/types/Number' +import { Boolean as BooleanAttr } from './model/attributes/types/Boolean' +import { Relation } from './model/attributes/relations/Relation' +import { HasOne as HasOneAttr } from './model/attributes/relations/HasOne' +import { HasMany as HasManyAttr } from './model/attributes/relations/HasMany' +import { mutations } from './modules/Mutations' +import { Repository } from './repository/Repository' +import { Interpretation } from './interpretation/Interpretation' +import { Query } from './query/Query' +import { Connection } from './connection/Connection' export default { install, Database, Schema, Model, - Attributes, - Mutations, + Attribute, + Type, + AttrAttr, + StringAttr, + NumberAttr, + BooleanAttr, + Relation, + HasOneAttr, + HasManyAttr, + mutations, Repository, Interpretation, Query, - QueryOptions, Connection } diff --git a/src/index.ts b/src/index.ts index 8c9edc98..e4c79481 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,65 +1,73 @@ import './polyfills/Polyfills' export * from './data/Data' +export * from './store/Store' +export * from './database/Database' +export * from './schema/Schema' +export * from './model/Model' +export * from './model/decorators/attributes/types/Attr' +export * from './model/decorators/attributes/types/Str' +export * from './model/decorators/attributes/types/Num' +export * from './model/decorators/attributes/types/Bool' +export * from './model/decorators/attributes/relations/HasOne' +export * from './model/decorators/attributes/relations/HasMany' +export * from './model/decorators/Contracts' +export * from './model/attributes/Attribute' +export * from './model/attributes/types/Type' +export { Attr as AttrAttr } from './model/attributes/types/Attr' +export { String as StringAttr } from './model/attributes/types/String' +export { Number as NumberAttr } from './model/attributes/types/Number' +export { Boolean as BooleanAttr } from './model/attributes/types/Boolean' +export * from './model/attributes/relations/Relation' +export { HasOne as HasOneAttr } from './model/attributes/relations/HasOne' +export { HasMany as HasManyAttr } from './model/attributes/relations/HasMany' +export * from './modules/RootModule' +export * from './modules/RootState' +export * from './modules/Module' +export * from './modules/State' +export * from './modules/Mutations' +export * from './repository/Repository' +export * from './interpretation/Interpretation' +export * from './query/Query' +export * from './query/Options' +export * from './connection/Connection' -import { install, Options } from './store/Store' -import Database from './database/Database' -import Schema from './schema/Schema' -import Model, { Fields } from './model/Model' -import * as Attributes from './model/attributes/Attributes' -import Attr from './model/decorators/attributes/types/Attr' -import Str from './model/decorators/attributes/types/Str' -import Num from './model/decorators/attributes/types/Num' -import Bool from './model/decorators/attributes/types/Bool' -import HasOne from './model/decorators/attributes/relations/HasOne' -import HasMany from './model/decorators/attributes/relations/HasMany' -import RootModule from './modules/RootModule' -import RootState from './modules/RootState' -import Module from './modules/Module' -import State from './modules/State' -import Mutations from './modules/Mutations' -import Repository from './repository/Repository' -import Interpretation from './interpretation/Interpretation' -import Query from './query/Query' -import * as QueryOptions from './query/options/Options' -import Connection from './connection/Connection' - -export { - install, - Options, - Database, - Schema, - Model, - Fields, - Attributes, - Attr, - Str, - Num, - Bool, - HasOne, - HasMany, - RootModule, - RootState, - Module, - State, - Mutations, - Repository, - Interpretation, - Query, - QueryOptions, - Connection -} +import { install } from './store/Store' +import { Database } from './database/Database' +import { Schema } from './schema/Schema' +import { Model } from './model/Model' +import { Attribute } from './model/attributes/Attribute' +import { Type } from './model/attributes/types/Type' +import { Attr as AttrAttr } from './model/attributes/types/Attr' +import { String as StringAttr } from './model/attributes/types/String' +import { Number as NumberAttr } from './model/attributes/types/Number' +import { Boolean as BooleanAttr } from './model/attributes/types/Boolean' +import { Relation } from './model/attributes/relations/Relation' +import { HasOne as HasOneAttr } from './model/attributes/relations/HasOne' +import { HasMany as HasManyAttr } from './model/attributes/relations/HasMany' +import { mutations } from './modules/Mutations' +import { Repository } from './repository/Repository' +import { Interpretation } from './interpretation/Interpretation' +import { Query } from './query/Query' +import { Connection } from './connection/Connection' export default { install, Database, Schema, Model, - Attributes, - Mutations, + Attribute, + Type, + AttrAttr, + StringAttr, + NumberAttr, + BooleanAttr, + Relation, + HasOneAttr, + HasManyAttr, + mutations, Repository, Interpretation, Query, - QueryOptions, Connection } diff --git a/src/interpretation/Interpretation.ts b/src/interpretation/Interpretation.ts index 9cdf50b2..c868c997 100644 --- a/src/interpretation/Interpretation.ts +++ b/src/interpretation/Interpretation.ts @@ -1,11 +1,11 @@ import { normalize, schema as Normalizr } from 'normalizr' import { Store } from 'vuex' import { isArray, isEmpty } from '../support/Utils' -import { Record, Records, NormalizedData } from '../data/Data' -import Relation from '../model/attributes/relations/Relation' -import Model from '../model/Model' +import { Element, Elements, NormalizedData } from '../data/Data' +import { Relation } from '../model/attributes/relations/Relation' +import { Model } from '../model/Model' -export default class Interpretation { +export class Interpretation { /** * The store instance. */ @@ -27,7 +27,7 @@ export default class Interpretation { /** * Perform interpretation for the given data. */ - process(data: Record | Record[]): NormalizedData { + process(data: Element | Element[]): NormalizedData { if (isEmpty(data)) { return {} } @@ -39,7 +39,7 @@ export default class Interpretation { return normalizedData } - private normalize(data: Record | Record[]): NormalizedData { + private normalize(data: Element | Element[]): NormalizedData { const schema = isArray(data) ? [this.getSchema()] : this.getSchema() return normalize(data, schema).entities as NormalizedData @@ -57,16 +57,16 @@ export default class Interpretation { */ private attach(data: NormalizedData): void { for (const entity in data) { - this.attachRecords(entity, data[entity], data) + this.attachElements(entity, data[entity], data) } } /** * Attach any missing foreign keys to the records. */ - private attachRecords( + private attachElements( entity: string, - records: Records, + records: Elements, data: NormalizedData ): void { const model = this.store.$database.getModel(entity) @@ -76,16 +76,16 @@ export default class Interpretation { } for (const id in records) { - this.attachRecord(model, records[id], data) + this.attachElement(model, records[id], data) } } /** * Attach any missing foreign keys to the record. */ - private attachRecord( + private attachElement( model: Model, - record: Record, + record: Element, data: NormalizedData ): void { for (const key in record) { diff --git a/src/model/Model.ts b/src/model/Model.ts index f1f61640..137f6e7c 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,28 +1,24 @@ import { Store } from 'vuex' -import { Record } from '../data/Data' -import * as Attributes from './attributes/Attributes' - -export interface Fields { - [key: string]: Attributes.Attribute -} - -interface Schemas { - [name: string]: Fields -} - -interface Registries { - [name: string]: Registry -} - -interface Registry { - [key: string]: () => Attributes.Attribute -} - -interface FillOptions { +import { Element } from '../data/Data' +import { Attribute } from './attributes/Attribute' +import { Attr } from './attributes/types/Attr' +import { String as Str } from './attributes/types/String' +import { Number as Num } from './attributes/types/Number' +import { Boolean as Bool } from './attributes/types/Boolean' +import { Relation } from './attributes/relations/Relation' +import { HasOne } from './attributes/relations/HasOne' +import { HasMany } from './attributes/relations/HasMany' + +export type ModelFields = Record +export type ModelSchemas = Record +export type ModelRegistries = Record +export type ModelRegistry = Record Attribute> + +export interface ModelOptions { relations?: boolean } -export default class Model { +export class Model { /** * The store instance. */ @@ -42,14 +38,14 @@ export default class Model { * The schema for the model. It contains the result of the `fields` * method or the attributes defined by decorators. */ - protected static schemas: Schemas = {} + protected static schemas: ModelSchemas = {} /** * The registry for the model. It contains predefined model schema generated * by the decorators, and gets evaluated and stored at `schema` property * when registering models to the database */ - protected static registries: Registries = {} + protected static registries: ModelRegistries = {} /** * The array of booted models. @@ -59,7 +55,7 @@ export default class Model { /** * Create a new model instance. */ - constructor(attributes?: Record, options?: FillOptions) { + constructor(attributes?: Element, options?: ModelOptions) { this.$boot() this.$fill(attributes, options) @@ -81,10 +77,7 @@ export default class Model { /** * Set the attribute to the registry. */ - static setRegistry( - key: string, - attribute: () => Attributes.Attribute - ): typeof Model { + static setRegistry(key: string, attribute: () => Attribute): typeof Model { if (!this.registries[this.entity]) { this.registries[this.entity] = {} } @@ -105,29 +98,29 @@ export default class Model { /** * Create a new attr attribute instance. */ - static attr(value: any): Attributes.Attr { - return new Attributes.Attr(new this(), value) + static attr(value: any): Attr { + return new Attr(new this(), value) } /** * Create a new string attribute instance. */ - static string(value: string | null): Attributes.String { - return new Attributes.String(new this(), value) + static string(value: string | null): Str { + return new Str(new this(), value) } /** * Create a new number attribute instance. */ - static number(value: number | null): Attributes.Number { - return new Attributes.Number(new this(), value) + static number(value: number | null): Num { + return new Num(new this(), value) } /** * Create a new boolean attribute instance. */ - static boolean(value: boolean | null): Attributes.Boolean { - return new Attributes.Boolean(new this(), value) + static boolean(value: boolean | null): Bool { + return new Bool(new this(), value) } /** @@ -137,15 +130,10 @@ export default class Model { related: typeof Model, foreignKey: string, localKey?: string - ): Attributes.HasOne { + ): HasOne { localKey = localKey ?? this.getLocalKey() - return new Attributes.HasOne( - new this(), - new related(), - foreignKey, - localKey - ) + return new HasOne(new this(), new related(), foreignKey, localKey) } /** @@ -155,15 +143,10 @@ export default class Model { related: typeof Model, foreignKey: string, localKey?: string - ): Attributes.HasMany { + ): HasMany { localKey = localKey ?? this.getLocalKey() - return new Attributes.HasMany( - new this(), - new related(), - foreignKey, - localKey - ) + return new HasMany(new this(), new related(), foreignKey, localKey) } /** @@ -215,7 +198,7 @@ export default class Model { * way for us to generate fresh model instances of this current model. It's * particularly useful during the hydration of new objects via the query. */ - $newInstance(attributes?: Record, options?: FillOptions): this { + $newInstance(attributes?: Element, options?: ModelOptions): this { const model = new this.$self(attributes, options) as this model.$setStore(model.$store) @@ -226,7 +209,7 @@ export default class Model { /** * Get model fields for the model. */ - get $fields(): Fields { + get $fields(): ModelFields { return this.$self.schemas[this.$entity] } @@ -249,17 +232,17 @@ export default class Model { } /** - * Fill the model by the given attributes. Its default values will fill any + * Fill the model by the given Its default values will fill any * missing field. */ - $fill(attributes: Record = {}, options: FillOptions = {}): this { + $fill(attributes: Element = {}, options: ModelOptions = {}): this { const fillRelation = options.relations ?? true for (const key in this.$fields) { const attr = this.$fields[key] const value = attributes[key] - if (attr instanceof Attributes.Relation && !fillRelation) { + if (attr instanceof Relation && !fillRelation) { continue } @@ -272,11 +255,7 @@ export default class Model { /** * Fill the model filed. */ - protected $fillField( - key: string, - attr: Attributes.Attribute, - value: any - ): void { + protected $fillField(key: string, attr: Attribute, value: any): void { if (value !== undefined) { this[key] = attr.make(value) return @@ -300,7 +279,7 @@ export default class Model { /** * Get the index id for the model or the given record. */ - $getIndexId(record?: Record): string { + $getIndexId(record?: Element): string { const target = record ?? this return String(target[this.$primaryKey]) @@ -313,7 +292,7 @@ export default class Model { let result = false for (const key in this.$fields) { - if (this.$fields[key] instanceof Attributes.Relation) { + if (this.$fields[key] instanceof Relation) { result = true } } @@ -324,10 +303,10 @@ export default class Model { /** * Get the relation instance for the given relation name. */ - $getRelation(name: string): Attributes.Relation { + $getRelation(name: string): Relation { const relation = this.$fields[name] - if (!(relation instanceof Attributes.Relation)) { + if (!(relation instanceof Relation)) { throw new Error( `[Vuex ORM] Relationship [${name}] on model [${this.$entity}] not found.` ) @@ -348,8 +327,8 @@ export default class Model { /** * Get the serialized model attributes. */ - $getAttributes(): Record { - const record = {} as Record + $getAttributes(): Element { + const record = {} as Element for (const key in this.$fields) { if (this[key] !== undefined) { diff --git a/src/model/attributes/Attribute.ts b/src/model/attributes/Attribute.ts index e11adbae..f044f834 100644 --- a/src/model/attributes/Attribute.ts +++ b/src/model/attributes/Attribute.ts @@ -1,6 +1,6 @@ -import Model from '../Model' +import { Model } from '../Model' -export default abstract class Attribute { +export abstract class Attribute { /** * The model instance. */ diff --git a/src/model/attributes/Attributes.ts b/src/model/attributes/Attributes.ts deleted file mode 100644 index ecdff443..00000000 --- a/src/model/attributes/Attributes.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Attribute from './Attribute' -import Type from './types/Type' -import Attr from './types/Attr' -import String from './types/String' -import Number from './types/Number' -import Boolean from './types/Boolean' -import Relation from './relations/Relation' -import HasOne from './relations/HasOne' -import HasMany from './relations/HasMany' - -export { - Attribute, - Type, - Attr, - String, - Number, - Boolean, - Relation, - HasOne, - HasMany -} diff --git a/src/model/attributes/relations/HasMany.ts b/src/model/attributes/relations/HasMany.ts index a02109a8..4a080bd3 100644 --- a/src/model/attributes/relations/HasMany.ts +++ b/src/model/attributes/relations/HasMany.ts @@ -1,11 +1,11 @@ import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../../data/Data' -import Query from '../../../query/Query' -import Model from '../../Model' -import Relation, { Dictionary } from './Relation' +import { Schema } from '../../../schema/Schema' +import { Element, NormalizedData, Collection } from '../../../data/Data' +import { Query } from '../../../query/Query' +import { Model } from '../../Model' +import { Relation, Dictionary } from './Relation' -export default class HasMany extends Relation { +export class HasMany extends Relation { /** * The foreign key of the parent model. */ @@ -40,12 +40,16 @@ export default class HasMany extends Relation { /** * Attach the relational key to the given data. */ - attach(ids: (string | number)[], record: Record, data: NormalizedData): void { + attach( + ids: (string | number)[], + record: Element, + data: NormalizedData + ): void { ids.forEach((id) => { - const relatedRecord = data[this.related.$entity]?.[id] + const relatedElement = data[this.related.$entity]?.[id] - if (relatedRecord) { - relatedRecord[this.foreignKey] = record[this.localKey] + if (relatedElement) { + relatedElement[this.foreignKey] = record[this.localKey] } }) } diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index 412c889a..8f0a076d 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -1,11 +1,11 @@ import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../../data/Data' -import Query from '../../../query/Query' -import Model from '../../Model' -import Relation, { Dictionary } from './Relation' +import { Schema } from '../../../schema/Schema' +import { Element, NormalizedData, Collection } from '../../../data/Data' +import { Query } from '../../../query/Query' +import { Model } from '../../Model' +import { Relation, Dictionary } from './Relation' -export default class HasOne extends Relation { +export class HasOne extends Relation { /** * The foreign key of the parent model. */ @@ -40,11 +40,11 @@ export default class HasOne extends Relation { /** * Attach the relational key to the given data. */ - attach(id: string | number, record: Record, data: NormalizedData): void { - const relatedRecord = data[this.related.$entity]?.[id] + attach(id: string | number, record: Element, data: NormalizedData): void { + const relatedElement = data[this.related.$entity]?.[id] - if (relatedRecord) { - relatedRecord[this.foreignKey] = record[this.localKey] + if (relatedElement) { + relatedElement[this.foreignKey] = record[this.localKey] } } diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts index 9c2f9e41..0b7a4be2 100644 --- a/src/model/attributes/relations/Relation.ts +++ b/src/model/attributes/relations/Relation.ts @@ -1,15 +1,15 @@ import { Schema as NormalizrSchema } from 'normalizr' -import Schema from '../../../schema/Schema' -import { Record, NormalizedData, Collection } from '../../../data/Data' -import Query from '../../../query/Query' -import Model from '../../Model' -import Attribute from '../Attribute' +import { Schema } from '../../../schema/Schema' +import { Element, NormalizedData, Collection } from '../../../data/Data' +import { Query } from '../../../query/Query' +import { Model } from '../../Model' +import { Attribute } from '../Attribute' export interface Dictionary { [id: string]: Model[] } -export default abstract class Relation extends Attribute { +export abstract class Relation extends Attribute { /** * The parent model. */ @@ -44,7 +44,7 @@ export default abstract class Relation extends Attribute { /** * Attach the relational key to the given data. */ - abstract attach(ids: any, record: Record, data: NormalizedData): void + abstract attach(ids: any, record: Element, data: NormalizedData): void /** * Set the constraints for an eager loading relation. diff --git a/src/model/attributes/types/Attr.ts b/src/model/attributes/types/Attr.ts index dd4b8dc5..410dd154 100644 --- a/src/model/attributes/types/Attr.ts +++ b/src/model/attributes/types/Attr.ts @@ -1,6 +1,6 @@ -import Type from './Type' +import { Type } from './Type' -export default class Attr extends Type { +export class Attr extends Type { /** * Make the value for the attribute. */ diff --git a/src/model/attributes/types/Boolean.ts b/src/model/attributes/types/Boolean.ts index d21e563a..cfe3f202 100644 --- a/src/model/attributes/types/Boolean.ts +++ b/src/model/attributes/types/Boolean.ts @@ -1,7 +1,7 @@ -import Model from '../../Model' -import Type from './Type' +import { Model } from '../../Model' +import { Type } from './Type' -export default class Boolean extends Type { +export class Boolean extends Type { /** * Create a new boolean attribute instance. */ diff --git a/src/model/attributes/types/Number.ts b/src/model/attributes/types/Number.ts index 755f5bf7..945b1b6a 100644 --- a/src/model/attributes/types/Number.ts +++ b/src/model/attributes/types/Number.ts @@ -1,7 +1,7 @@ -import Model from '../../Model' -import Type from './Type' +import { Model } from '../../Model' +import { Type } from './Type' -export default class Number extends Type { +export class Number extends Type { /** * Create a new number attribute instance. */ diff --git a/src/model/attributes/types/String.ts b/src/model/attributes/types/String.ts index 52e2d006..d0f5485e 100644 --- a/src/model/attributes/types/String.ts +++ b/src/model/attributes/types/String.ts @@ -1,7 +1,7 @@ -import Model from '../../Model' -import Type from './Type' +import { Model } from '../../Model' +import { Type } from './Type' -export default class String extends Type { +export class String extends Type { /** * Create a new string attribute instance. */ diff --git a/src/model/attributes/types/Type.ts b/src/model/attributes/types/Type.ts index 2ef34718..1ca2886c 100644 --- a/src/model/attributes/types/Type.ts +++ b/src/model/attributes/types/Type.ts @@ -1,7 +1,7 @@ -import Model from '../../Model' -import Attribute from '../Attribute' +import { Model } from '../../Model' +import { Attribute } from '../Attribute' -export default abstract class Type extends Attribute { +export abstract class Type extends Attribute { /** * The default value for the attribute. */ diff --git a/src/model/decorators/Contracts.ts b/src/model/decorators/Contracts.ts index 88fb5672..43a78e44 100644 --- a/src/model/decorators/Contracts.ts +++ b/src/model/decorators/Contracts.ts @@ -1,4 +1,4 @@ -import Model from '../Model' +import { Model } from '../Model' export type PropertyDecorator = (target: Model, propertyKey: string) => void diff --git a/src/model/decorators/attributes/relations/HasMany.ts b/src/model/decorators/attributes/relations/HasMany.ts index 850ff621..20fc0d1a 100644 --- a/src/model/decorators/attributes/relations/HasMany.ts +++ b/src/model/decorators/attributes/relations/HasMany.ts @@ -1,10 +1,10 @@ -import Model from '../../../Model' +import { Model } from '../../../Model' import { PropertyDecorator } from '../../Contracts' /** * Create a has many decorator. */ -export default function HasMany( +export function HasMany( related: () => typeof Model, foreignKey: string, localKey?: string diff --git a/src/model/decorators/attributes/relations/HasOne.ts b/src/model/decorators/attributes/relations/HasOne.ts index e2f6cc3e..b05f0823 100644 --- a/src/model/decorators/attributes/relations/HasOne.ts +++ b/src/model/decorators/attributes/relations/HasOne.ts @@ -1,10 +1,10 @@ -import Model from '../../../Model' +import { Model } from '../../../Model' import { PropertyDecorator } from '../../Contracts' /** * Create a has one decorator. */ -export default function HasOne( +export function HasOne( related: () => typeof Model, foreignKey: string, localKey?: string diff --git a/src/model/decorators/attributes/types/Attr.ts b/src/model/decorators/attributes/types/Attr.ts index 2e946b0b..b0f1d00d 100644 --- a/src/model/decorators/attributes/types/Attr.ts +++ b/src/model/decorators/attributes/types/Attr.ts @@ -3,7 +3,7 @@ import { PropertyDecorator } from '../../Contracts' /** * Create an attr decorator. */ -export default function Attr(value?: any): PropertyDecorator { +export function Attr(value?: any): PropertyDecorator { return (target, propertyKey) => { target.$self.setRegistry(propertyKey, () => target.$self.attr(value)) } diff --git a/src/model/decorators/attributes/types/Bool.ts b/src/model/decorators/attributes/types/Bool.ts index a091ffec..9ed5c80d 100644 --- a/src/model/decorators/attributes/types/Bool.ts +++ b/src/model/decorators/attributes/types/Bool.ts @@ -3,7 +3,7 @@ import { PropertyDecorator, TypeOptions } from '../../Contracts' /** * Create a str decorator. */ -export default function Str( +export function Bool( value: boolean | null, options: TypeOptions = {} ): PropertyDecorator { diff --git a/src/model/decorators/attributes/types/Num.ts b/src/model/decorators/attributes/types/Num.ts index 78404ac8..9cb7265f 100644 --- a/src/model/decorators/attributes/types/Num.ts +++ b/src/model/decorators/attributes/types/Num.ts @@ -3,7 +3,7 @@ import { PropertyDecorator, TypeOptions } from '../../Contracts' /** * Create a str decorator. */ -export default function Str( +export function Num( value: number | null, options: TypeOptions = {} ): PropertyDecorator { diff --git a/src/model/decorators/attributes/types/Str.ts b/src/model/decorators/attributes/types/Str.ts index 763c1775..6bc2dfe8 100644 --- a/src/model/decorators/attributes/types/Str.ts +++ b/src/model/decorators/attributes/types/Str.ts @@ -3,7 +3,7 @@ import { PropertyDecorator, TypeOptions } from '../../Contracts' /** * Create a str decorator. */ -export default function Str( +export function Str( value: string | null, options: TypeOptions = {} ): PropertyDecorator { diff --git a/src/modules/Module.ts b/src/modules/Module.ts index fdbad937..186e62bd 100644 --- a/src/modules/Module.ts +++ b/src/modules/Module.ts @@ -1,7 +1,9 @@ import { Module as BaseModule } from 'vuex' -import State from './State' +import { State } from './State' +import { Mutations } from './Mutations' -export default interface Module extends BaseModule { +export interface Module extends BaseModule { namespaced: true state: S + mutations: Mutations } diff --git a/src/modules/Mutations.ts b/src/modules/Mutations.ts index f8582e69..554a4a69 100644 --- a/src/modules/Mutations.ts +++ b/src/modules/Mutations.ts @@ -1,9 +1,13 @@ import { MutationTree } from 'vuex' -import { Records } from '../data/Data' -import State from './State' +import { Elements } from '../data/Data' +import { State } from './State' export interface Mutations extends MutationTree { - mutate(state: State, callback: (state: S) => void): void + mutate(state: S, callback: (state: S) => void): void + insert(state: S, records: Elements): void + update(state: S, records: Elements): void + delete(state: S, ids: string[]): void + deleteAll(state: S): void } /** @@ -16,14 +20,14 @@ function mutate(state: State, callback: (state: State) => void): void { /** * Commit `insert` change to the store. */ -function insert(state: State, records: Records): void { +function insert(state: State, records: Elements): void { state.data = { ...state.data, ...records } } /** * Commit `update` change to the store. */ -function update(state: State, records: Records): void { +function update(state: State, records: Elements): void { state.data = { ...state.data, ...records } } @@ -49,7 +53,7 @@ function destroyAll(state: State): void { state.data = {} } -export default { +export const mutations = { mutate, insert, update, diff --git a/src/modules/RootModule.ts b/src/modules/RootModule.ts index abf17a84..b132d62f 100644 --- a/src/modules/RootModule.ts +++ b/src/modules/RootModule.ts @@ -1,8 +1,7 @@ import { Module as BaseModule, ModuleTree } from 'vuex' -import RootState from './RootState' +import { RootState } from './RootState' -export default interface RootModule - extends BaseModule { +export interface RootModule extends BaseModule { namespaced: true modules: ModuleTree } diff --git a/src/modules/RootState.ts b/src/modules/RootState.ts index f5bf666a..d4382cd4 100644 --- a/src/modules/RootState.ts +++ b/src/modules/RootState.ts @@ -1,5 +1,5 @@ -import State from './State' +import { State } from './State' -export default interface RootState { +export interface RootState { [entity: string]: State } diff --git a/src/modules/State.ts b/src/modules/State.ts index c0a9972a..89abc503 100644 --- a/src/modules/State.ts +++ b/src/modules/State.ts @@ -1,5 +1,5 @@ -import { Records } from '../data/Data' +import { Elements } from '../data/Data' -export default interface State { - data: Records +export interface State { + data: Elements } diff --git a/src/query/options/Options.ts b/src/query/Options.ts similarity index 89% rename from src/query/options/Options.ts rename to src/query/Options.ts index 19c49f43..f072f2a3 100644 --- a/src/query/options/Options.ts +++ b/src/query/Options.ts @@ -1,4 +1,4 @@ -import Query from '../Query' +import { Query } from './Query' export interface Where { field: string | number diff --git a/src/query/Query.ts b/src/query/Query.ts index a9ba8693..648d592a 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -1,12 +1,12 @@ import { Store } from 'vuex' import { isArray, isEmpty, groupBy } from '../support/Utils' -import { Record, Item, Collection } from '../data/Data' -import { Relation } from '../model/attributes/Attributes' -import Model from '../model/Model' -import Connection from '../connection/Connection' -import * as Options from './options/Options' +import { Element, Item, Collection } from '../data/Data' +import { Relation } from '../model/attributes/relations/Relation' +import { Model } from '../model/Model' +import { Connection } from '../connection/Connection' +import { Where, EagerLoad, EagerLoadConstraint } from './Options' -export default class Query { +export class Query { /** * The store instance. */ @@ -20,7 +20,7 @@ export default class Query { /** * The where constraints for the query. */ - protected wheres: Options.Where[] = [] + protected wheres: Where[] = [] /** * The maximum number of records to return. @@ -35,7 +35,7 @@ export default class Query { /** * The relationships that should be eager loaded. */ - protected eagerLoad: Options.EagerLoad = {} + protected eagerLoad: EagerLoad = {} /** * Create a new query instance. @@ -152,20 +152,20 @@ export default class Query { const model = this.findRaw(ids) - return model ? this.hydrateRecord(model) : null + return model ? this.hydrateElement(model) : null } /** * Find multiple models by their primary keys. */ findIn(ids: (string | number)[]): Collection { - return this.hydrateRecords(this.findInRaw(ids)) + return this.hydrateElements(this.findInRaw(ids)) } /** * Find a record by its primary key. */ - findRaw(id: string | number): Record | null { + findRaw(id: string | number): Element | null { const indexId = this.normalizeIndexId(id) return this.connection().find(indexId) @@ -174,7 +174,7 @@ export default class Query { /** * Find multiple records by their primary keys. */ - findInRaw(ids: (string | number)[]): Record[] { + findInRaw(ids: (string | number)[]): Element[] { const indexIds = this.normalizeIndexIds(ids) return this.connection().findIn(indexIds) @@ -190,7 +190,7 @@ export default class Query { const collection = [] as Collection for (const id in records) { - collection.push(this.hydrateRecord(records[id])) + collection.push(this.hydrateElement(records[id])) } return collection @@ -240,7 +240,7 @@ export default class Query { /** * The function to compare where clause to the given model. */ - protected whereComparator(model: M, where: Options.Where): boolean { + protected whereComparator(model: M, where: Where): boolean { if (isArray(where.value)) { return where.value.includes(model[where.field]) } @@ -272,7 +272,7 @@ export default class Query { protected eagerLoadRelation( models: Collection, name: string, - constraints: Options.EagerLoadConstraint + constraints: EagerLoadConstraint ): void { // First we will "back up" the existing where conditions on the query so we can // add our eager constraints. Then we will merge the wheres that were on the @@ -301,8 +301,8 @@ export default class Query { /** * Insert the given record to the store. */ - async insert(records: Record[]): Promise> { - const models = this.hydrateRecords(records) + async insert(records: Element[]): Promise> { + const models = this.hydrateElements(records) this.connection().insert(this.dehydrateModels(models)) @@ -312,7 +312,7 @@ export default class Query { /** * Update records in the store by using the primary key of the given records. */ - async merge(records: Record[]): Promise> { + async merge(records: Element[]): Promise> { const models = this.getMergedModels(records) this.connection().update(this.dehydrateModels(models)) @@ -324,11 +324,11 @@ export default class Query { * Get models by merging the records. This method will use the primary key * in the records to fetch models and merge the given record to the model. */ - protected getMergedModels(records: Record[]): Collection { + protected getMergedModels(records: Element[]): Collection { return records.reduce>((collection, record) => { const model = this.find(this.model.$getIndexId(record)) - model && collection.push(this.mergeModelWithRecord(model, record)) + model && collection.push(this.mergeModelWithElement(model, record)) return collection }, []) @@ -337,8 +337,8 @@ export default class Query { /** * Update records in the store. */ - async update(record: Record): Promise> { - const models = this.mergeModelsWithRecord(this.get(), record) + async update(record: Element): Promise> { + const models = this.mergeModelsWithElement(this.get(), record) this.connection().update(this.dehydrateModels(models)) @@ -413,37 +413,37 @@ export default class Query { /** * Instantiate new models with the given record. */ - protected hydrateRecord(record: Record): M { + protected hydrateElement(record: Element): M { return this.model.$newInstance(record, { relations: false }) } /** * Instantiate new models with the given collection of records. */ - protected hydrateRecords(records: Record[]): Collection { - return records.map((record) => this.hydrateRecord(record)) + protected hydrateElements(records: Element[]): Collection { + return records.map((record) => this.hydrateElement(record)) } /** * Convert all models into the plain record. */ - protected dehydrateModels(models: Collection): Record[] { + protected dehydrateModels(models: Collection): Element[] { return models.map((model) => model.$getAttributes()) } /** * Merge the model with the given record. */ - protected mergeModelWithRecord(model: M, record: Record): M { + protected mergeModelWithElement(model: M, record: Element): M { return model.$fill(record) } /** * Merge models with the given record. */ - protected mergeModelsWithRecord( + protected mergeModelsWithElement( models: Collection, - record: Record + record: Element ): Collection { return models.map((model) => model.$fill(record)) } diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 843f23f8..7c190d9c 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -1,18 +1,25 @@ import { Store } from 'vuex' import { Constructor } from '../types' -import * as Data from '../data/Data' -import Model from '../model/Model' -import Interpretation from '../interpretation/Interpretation' -import Query from '../query/Query' - -type PersistMethod = 'insert' | 'merge' - -interface CollectionPromises { +import { + Element, + Elements, + NormalizedData, + Item, + Collection, + Collections +} from '../data/Data' +import { Model } from '../model/Model' +import { Interpretation } from '../interpretation/Interpretation' +import { Query } from '../query/Query' + +export type PersistMethod = 'insert' | 'merge' + +export interface CollectionPromises { indexes: string[] - promises: Promise>[] + promises: Promise>[] } -export default class Repository { +export class Repository { /** * The store instance. */ @@ -78,30 +85,30 @@ export default class Repository { /** * Get all models from the store. */ - all(): Data.Collection { + all(): Collection { return this.query().get() } /** * Find the model with the given id. */ - find(id: string | number): Data.Item - find(ids: (string | number)[]): Data.Collection - find(ids: any): Data.Item { + find(id: string | number): Item + find(ids: (string | number)[]): Collection + find(ids: any): Item { return this.query().find(ids) } /** * Insert the given record to the store. */ - insert(record: Data.Record | Data.Record[]): Promise { + insert(record: Element | Element[]): Promise { return this.persist('insert', record) } /** * Update records in the store. */ - update(record: Data.Record | Data.Record[]): Promise { + update(record: Element | Element[]): Promise { return this.persist('merge', record) } @@ -110,8 +117,8 @@ export default class Repository { */ protected persist( method: PersistMethod, - record: Data.Record | Data.Record[] - ): Promise { + record: Element | Element[] + ): Promise { const normalizedData = this.interpret(record) const { indexes, promises } = this.createCollectionPromises( @@ -125,10 +132,10 @@ export default class Repository { /** * Persist normalized records with the given method. */ - protected persistRecords( + protected persistElements( method: PersistMethod, - records: Data.Records - ): Promise> { + records: Elements + ): Promise> { return this.query()[method](this.mapNormalizedData(records)) } @@ -137,17 +144,17 @@ export default class Repository { */ protected createCollectionPromises( method: PersistMethod, - data: Data.NormalizedData + data: NormalizedData ): CollectionPromises { const indexes: string[] = [] - const promises: Promise>[] = [] + const promises: Promise>[] = [] for (const entity in data) { const records = data[entity] const repository = this.newRepository(entity) indexes.push(entity) - promises.push(repository.persistRecords(method, records)) + promises.push(repository.persistElements(method, records)) } return { indexes, promises } @@ -158,9 +165,9 @@ export default class Repository { */ protected async resolveCollectionPromises( indexes: string[], - promises: Promise>[] - ): Promise { - return (await Promise.all(promises)).reduce( + promises: Promise>[] + ): Promise { + return (await Promise.all(promises)).reduce( (collections, collection, index) => { collections[indexes[index]] = collection return collections @@ -172,8 +179,8 @@ export default class Repository { /** * Convert normalized data into an array of records. */ - protected mapNormalizedData(records: Data.Records): Data.Record[] { - const items = [] as Data.Record[] + protected mapNormalizedData(records: Elements): Element[] { + const items = [] as Element[] for (const id in records) { items.push(records[id]) @@ -185,17 +192,15 @@ export default class Repository { /** * Normalize the given record. */ - protected interpret( - records: Data.Record | Data.Record[] - ): Data.NormalizedData { + protected interpret(records: Element | Element[]): NormalizedData { return this.interpretation().process(records) } /** * Destroy the models for the given id. */ - destroy(id: string | number): Promise> - destroy(ids: (string | number)[]): Promise> + destroy(id: string | number): Promise> + destroy(ids: (string | number)[]): Promise> destroy(ids: any): Promise { return this.query().destroy(ids) } @@ -203,7 +208,7 @@ export default class Repository { /** * Delete all records in the store. */ - deleteAll(): Promise> { + deleteAll(): Promise> { return this.query().deleteAll() } } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index d87786ea..ec77e3c6 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -1,16 +1,12 @@ import { schema as Normalizr, Schema as NormalizrSchema } from 'normalizr' -import { Relation } from '../model/attributes/Attributes' -import Model from '../model/Model' +import { Relation } from '../model/attributes/relations/Relation' +import { Model } from '../model/Model' -interface Schemas { - [entity: string]: Normalizr.Entity -} - -export default class Schema { +export class Schema { /** * List of generated schemas. */ - private schemas: Schemas = {} + private schemas: Record = {} /** * The model instance. diff --git a/src/store/Store.ts b/src/store/Store.ts index a2920267..23e9aaa6 100644 --- a/src/store/Store.ts +++ b/src/store/Store.ts @@ -1,19 +1,22 @@ import { Constructor } from '../types' import { Store, Plugin } from 'vuex' -import Database from '../database/Database' -import Model from '../model/Model' -import Repository from '../repository/Repository' +import { Database } from '../database/Database' +import { Model } from '../model/Model' +import { Repository } from '../repository/Repository' -export interface Options { +export interface InstallOptions { namespace?: string } -type FilledOptions = Required +type FilledInstallOptions = Required /** * Install Vuex ORM to the store. */ -export function install(database: Database, options?: Options): Plugin { +export function install( + database: Database, + options?: InstallOptions +): Plugin { return (store) => { mixin(store, database, createOptions(options)) } @@ -22,7 +25,7 @@ export function install(database: Database, options?: Options): Plugin { /** * Create options by merging the given user-provided options. */ -function createOptions(options: Options = {}): FilledOptions { +function createOptions(options: InstallOptions = {}): FilledInstallOptions { return { namespace: options.namespace ?? 'entities' } @@ -34,7 +37,7 @@ function createOptions(options: Options = {}): FilledOptions { function mixin( store: Store, database: Database, - options: FilledOptions + options: FilledInstallOptions ): void { connectDatabase(store, database, options) @@ -49,7 +52,7 @@ function mixin( function connectDatabase( store: Store, database: Database, - options: FilledOptions + options: FilledInstallOptions ): void { database.setStore(store).setConnection(options.namespace) @@ -67,7 +70,7 @@ function startDatabase(store: Store): void { * Mixin repo function to the store. */ function mixinRepoFunction(store: Store): void { - store.$repo = function( + store.$repo = function ( model: Constructor ): Repository { return new Repository(this, model) diff --git a/src/types/vuex.d.ts b/src/types/vuex.d.ts index 87bdc1be..a537c73d 100644 --- a/src/types/vuex.d.ts +++ b/src/types/vuex.d.ts @@ -1,7 +1,7 @@ import { Constructor } from '../types' -import Database from '../database/Database' -import Model from '../model/Model' -import Repository from '../repository/Repository' +import { Database } from '../database/Database' +import { Model } from '../model/Model' +import { Repository } from '../repository/Repository' declare module 'vuex' { interface Store { diff --git a/test/Helpers.ts b/test/Helpers.ts index d1896282..c204b2ee 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -1,19 +1,19 @@ import Vue from 'vue' import Vuex, { Store } from 'vuex' import VuexORM, { + Element, + Elements, + Collection, Database, Model, RootState, - State, - Record, - Records, - Collection + State } from '@/index' Vue.use(Vuex) interface Entities { - [name: string]: Records + [name: string]: Elements } export function createStore(models: typeof Model[]): Store { @@ -53,13 +53,13 @@ export function assertState(store: Store, entities: Entities): void { expect(store.state.entities).toEqual(createState(entities)) } -export function assertModel(model: M, record: Record): void { +export function assertModel(model: M, record: Element): void { expect(model.$getAttributes()).toEqual(record) } export function assertModels( models: Collection, - record: Record[] + record: Element[] ): void { models.forEach((model, index) => { expect(model.$getAttributes()).toEqual(record[index]) diff --git a/test/feature/relations/types/has_many_retrieve.spec.ts b/test/feature/relations/types/has_many_retrieve.spec.ts index 04f3be51..2ced3592 100644 --- a/test/feature/relations/types/has_many_retrieve.spec.ts +++ b/test/feature/relations/types/has_many_retrieve.spec.ts @@ -38,10 +38,7 @@ describe('feature/relations/types/has_many_retrieve', () => { } }) - const user = store - .$repo(User) - .with('posts') - .first()! + const user = store.$repo(User).with('posts').first()! expect(user).toBeInstanceOf(User) assertInstanceOf(user.posts, Post) @@ -65,10 +62,7 @@ describe('feature/relations/types/has_many_retrieve', () => { posts: {} }) - const user = store - .$repo(User) - .with('posts') - .first()! + const user = store.$repo(User).with('posts').first()! expect(user).toBeInstanceOf(User) assertModel(user, { diff --git a/test/feature/relations/types/has_one_retrieve.spec.ts b/test/feature/relations/types/has_one_retrieve.spec.ts index b320061e..69cc8a4a 100644 --- a/test/feature/relations/types/has_one_retrieve.spec.ts +++ b/test/feature/relations/types/has_one_retrieve.spec.ts @@ -32,10 +32,7 @@ describe('feature/relations/types/has_one_retrieve', () => { } }) - const user = store - .$repo(User) - .with('phone') - .first()! + const user = store.$repo(User).with('phone').first()! expect(user).toBeInstanceOf(User) expect(user.phone).toBeInstanceOf(Phone) @@ -60,10 +57,7 @@ describe('feature/relations/types/has_one_retrieve', () => { phones: {} }) - const user = store - .$repo(User) - .with('phone') - .first()! + const user = store.$repo(User).with('phone').first()! expect(user).toBeInstanceOf(User) assertModel(user, { diff --git a/test/feature/repository/deletes_delete.spec.ts b/test/feature/repository/deletes_delete.spec.ts index 924b3724..d63f914a 100644 --- a/test/feature/repository/deletes_delete.spec.ts +++ b/test/feature/repository/deletes_delete.spec.ts @@ -20,10 +20,7 @@ describe('feature/repository/deletes_delete', () => { } }) - await store - .$repo(User) - .where('name', 'Jane Doe') - .delete() + await store.$repo(User).where('name', 'Jane Doe').delete() assertState(store, { users: { diff --git a/test/feature/repository/retrieves_where.spec.ts b/test/feature/repository/retrieves_where.spec.ts index 2c941167..4432fa2b 100644 --- a/test/feature/repository/retrieves_where.spec.ts +++ b/test/feature/repository/retrieves_where.spec.ts @@ -26,10 +26,7 @@ describe('feature/repository/retrieves_where', () => { } }) - const users = store - .$repo(User) - .where('age', 30) - .get() + const users = store.$repo(User).where('age', 30).get() const expected = [ { id: 1, name: 'John Doe', age: 30 }, diff --git a/test/feature/repository/retrieves_where_or.spec.ts b/test/feature/repository/retrieves_where_or.spec.ts index 68bb6287..bba31a99 100644 --- a/test/feature/repository/retrieves_where_or.spec.ts +++ b/test/feature/repository/retrieves_where_or.spec.ts @@ -53,10 +53,7 @@ describe('feature/repository/retrieves_where', () => { } }) - const users = store - .$repo(User) - .orWhere('age', 30) - .get() + const users = store.$repo(User).orWhere('age', 30).get() const expected = [{ id: 2, name: 'Jane Doe', age: 30 }] diff --git a/test/feature/repository/updates_update_query.spec.ts b/test/feature/repository/updates_update_query.spec.ts index 7655449f..986b23ca 100644 --- a/test/feature/repository/updates_update_query.spec.ts +++ b/test/feature/repository/updates_update_query.spec.ts @@ -21,10 +21,7 @@ describe('feature/repository/updates_update_query', () => { } }) - await store - .$repo(User) - .where('name', 'Jane Doe') - .update({ age: 50 }) + await store.$repo(User).where('name', 'Jane Doe').update({ age: 50 }) assertState(store, { users: { diff --git a/test/unit/model/Model_Attrs_Boolean.spec.ts b/test/unit/model/Model_Attrs_Boolean.spec.ts index c9c8e57c..76ae3f6c 100644 --- a/test/unit/model/Model_Attrs_Boolean.spec.ts +++ b/test/unit/model/Model_Attrs_Boolean.spec.ts @@ -1,5 +1,5 @@ -import Bool from '@/model/decorators/attributes/types/Bool' -import Model from '@/model/Model' +import { Bool } from '@/model/decorators/attributes/types/Bool' +import { Model } from '@/model/Model' describe('unit/model/Model_Attrs_Boolean', () => { it('casts the value to `Boolean` when instantiating the model', () => { diff --git a/test/unit/model/Model_Attrs_Number.spec.ts b/test/unit/model/Model_Attrs_Number.spec.ts index 012c5ee1..e773da17 100644 --- a/test/unit/model/Model_Attrs_Number.spec.ts +++ b/test/unit/model/Model_Attrs_Number.spec.ts @@ -1,5 +1,5 @@ -import Num from '@/model/decorators/attributes/types/Num' -import Model from '@/model/Model' +import { Num } from '@/model/decorators/attributes/types/Num' +import { Model } from '@/model/Model' describe('unit/model/Model_Attrs_Number', () => { it('casts the value to `Number` when instantiating the model', () => { diff --git a/test/unit/model/Model_Attrs_String.spec.ts b/test/unit/model/Model_Attrs_String.spec.ts index 3e1162e8..4ceab73b 100644 --- a/test/unit/model/Model_Attrs_String.spec.ts +++ b/test/unit/model/Model_Attrs_String.spec.ts @@ -1,5 +1,5 @@ -import Str from '@/model/decorators/attributes/types/Str' -import Model from '@/model/Model' +import { Str } from '@/model/decorators/attributes/types/Str' +import { Model } from '@/model/Model' describe('unit/model/Model_Attrs_String', () => { it('casts the value to `String` when instantiating the model', () => { diff --git a/tsconfig.json b/tsconfig.json index d675da58..811b06df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,9 @@ "baseUrl": ".", "rootDir": ".", "outDir": "dist", - "declaration": true, "sourceMap": true, + "declaration": true, + "declarationMap": true, "removeComments": false, "noImplicitAny": true, "noImplicitReturns": true, diff --git a/yarn.lock b/yarn.lock index 8f6683d2..8f90a526 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,20 +296,27 @@ "@babel/helper-create-regexp-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0": +"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-bigint@^7.0.0": +"@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" + integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-decorators@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz#8d2c15a9f1af624b0025f961682a9d53d3001bda" @@ -324,7 +331,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0": +"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== @@ -338,20 +345,48 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0": +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" + integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" + integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.0": +"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" @@ -711,43 +746,43 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^25.2.3": - version "25.2.3" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.2.3.tgz#38ac19b916ff61457173799239472659e1a67c39" - integrity sha512-k+37B1aSvOt9tKHWbZZSOy1jdgzesB0bj96igCVUG1nAH1W5EoUfgc5EXbBVU08KSLvkVdWopLXaO3xfVGlxtQ== +"@jest/console@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.3.0.tgz#33b56b81238427bf3ebe3f7b3378d2f79cdbd409" + integrity sha512-LvSDNqpmZIZyweFaEQ6wKY7CbexPitlsLHGJtcooNECo0An/w49rFhjCJzu6efeb6+a3ee946xss1Jcd9r03UQ== dependencies: - "@jest/source-map" "^25.2.1" + "@jest/source-map" "^25.2.6" chalk "^3.0.0" - jest-util "^25.2.3" + jest-util "^25.3.0" slash "^3.0.0" -"@jest/core@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.2.4.tgz#382ef80369d3311f1df79db1ee19e958ae95cdad" - integrity sha512-WcWYShl0Bqfcb32oXtjwbiR78D/djhMdJW+ulp4/bmHgeODcsieqUJfUH+kEv8M7VNV77E6jds5aA+WuGh1nmg== - dependencies: - "@jest/console" "^25.2.3" - "@jest/reporters" "^25.2.4" - "@jest/test-result" "^25.2.4" - "@jest/transform" "^25.2.4" - "@jest/types" "^25.2.3" +"@jest/core@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.3.0.tgz#80f97a7a8b59dde741a24f30871cc26d0197d426" + integrity sha512-+D5a/tFf6pA/Gqft2DLBp/yeSRgXhlJ+Wpst0X/ZkfTRP54qDR3C61VfHwaex+GzZBiTcE9vQeoZ2v5T10+Mqw== + dependencies: + "@jest/console" "^25.3.0" + "@jest/reporters" "^25.3.0" + "@jest/test-result" "^25.3.0" + "@jest/transform" "^25.3.0" + "@jest/types" "^25.3.0" ansi-escapes "^4.2.1" chalk "^3.0.0" exit "^0.1.2" graceful-fs "^4.2.3" - jest-changed-files "^25.2.3" - jest-config "^25.2.4" - jest-haste-map "^25.2.3" - jest-message-util "^25.2.4" - jest-regex-util "^25.2.1" - jest-resolve "^25.2.3" - jest-resolve-dependencies "^25.2.4" - jest-runner "^25.2.4" - jest-runtime "^25.2.4" - jest-snapshot "^25.2.4" - jest-util "^25.2.3" - jest-validate "^25.2.3" - jest-watcher "^25.2.4" + jest-changed-files "^25.3.0" + jest-config "^25.3.0" + jest-haste-map "^25.3.0" + jest-message-util "^25.3.0" + jest-regex-util "^25.2.6" + jest-resolve "^25.3.0" + jest-resolve-dependencies "^25.3.0" + jest-runner "^25.3.0" + jest-runtime "^25.3.0" + jest-snapshot "^25.3.0" + jest-util "^25.3.0" + jest-validate "^25.3.0" + jest-watcher "^25.3.0" micromatch "^4.0.2" p-each-series "^2.1.0" realpath-native "^2.0.0" @@ -755,36 +790,36 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.2.4.tgz#74f4d8dd87b427434d0b822cde37bc0e78f3e28b" - integrity sha512-wA4xlhD19/gukkDpJ5HQsTle0pgnzI5qMFEjw267lpTDC8d9N7Ihqr5pI+l0p8Qn1SQhai+glSqxrGdzKy4jxw== +"@jest/environment@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.3.0.tgz#587f28ddb4b0dfe97404d3d4a4c9dbfa0245fb2e" + integrity sha512-vgooqwJTHLLak4fE+TaCGeYP7Tz1Y3CKOsNxR1sE0V3nx3KRUHn3NUnt+wbcfd5yQWKZQKAfW6wqbuwQLrXo3g== dependencies: - "@jest/fake-timers" "^25.2.4" - "@jest/types" "^25.2.3" - jest-mock "^25.2.3" + "@jest/fake-timers" "^25.3.0" + "@jest/types" "^25.3.0" + jest-mock "^25.3.0" -"@jest/fake-timers@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.2.4.tgz#6821b6edde74fda2a42467ae92cc93095d4c9527" - integrity sha512-oC1TJiwfMcBttVN7Wz+VZnqEAgYTiEMu0QLOXpypR89nab0uCB31zm/QeBZddhSstn20qe3yqOXygp6OwvKT/Q== +"@jest/fake-timers@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.3.0.tgz#995aad36d5c8984165ca5db12e740ab8dbf7042a" + integrity sha512-NHAj7WbsyR3qBJPpBwSwqaq2WluIvUQsyzpJTN7XDVk7VnlC/y1BAnaYZL3vbPIP8Nhm0Ae5DJe0KExr/SdMJQ== dependencies: - "@jest/types" "^25.2.3" - jest-message-util "^25.2.4" - jest-mock "^25.2.3" - jest-util "^25.2.3" + "@jest/types" "^25.3.0" + jest-message-util "^25.3.0" + jest-mock "^25.3.0" + jest-util "^25.3.0" lolex "^5.0.0" -"@jest/reporters@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.2.4.tgz#aa01c20aab217150d3a6080d5c98ce0bf34b17ed" - integrity sha512-VHbLxM03jCc+bTLOluW/IqHR2G0Cl0iATwIQbuZtIUast8IXO4fD0oy4jpVGpG5b20S6REA8U3BaQoCW/CeVNQ== +"@jest/reporters@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.3.0.tgz#7f39f0e6911561cc5112a1b54656de18faee269b" + integrity sha512-1u0ZBygs0C9DhdYgLCrRfZfNKQa+9+J7Uo+Z9z0RWLHzgsxhoG32lrmMOtUw48yR6bLNELdvzormwUqSk4H4Vg== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^25.2.3" - "@jest/test-result" "^25.2.4" - "@jest/transform" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/console" "^25.3.0" + "@jest/test-result" "^25.3.0" + "@jest/transform" "^25.3.0" + "@jest/types" "^25.3.0" chalk "^3.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -793,11 +828,11 @@ istanbul-lib-instrument "^4.0.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.0" - jest-haste-map "^25.2.3" - jest-resolve "^25.2.3" - jest-util "^25.2.3" - jest-worker "^25.2.1" + istanbul-reports "^3.0.2" + jest-haste-map "^25.3.0" + jest-resolve "^25.3.0" + jest-util "^25.3.0" + jest-worker "^25.2.6" slash "^3.0.0" source-map "^0.6.0" string-length "^3.1.0" @@ -806,51 +841,50 @@ optionalDependencies: node-notifier "^6.0.0" -"@jest/source-map@^25.2.1": - version "25.2.1" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.2.1.tgz#b62ecf8ae76170b08eff8859b56eb7576df34ab8" - integrity sha512-PgScGJm1U27+9Te/cxP4oUFqJ2PX6NhBL2a6unQ7yafCgs8k02c0LSyjSIx/ao0AwcAdCczfAPDf5lJ7zoB/7A== +"@jest/source-map@^25.2.6": + version "25.2.6" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.2.6.tgz#0ef2209514c6d445ebccea1438c55647f22abb4c" + integrity sha512-VuIRZF8M2zxYFGTEhkNSvQkUKafQro4y+mwUxy5ewRqs5N/ynSFUODYp3fy1zCnbCMy1pz3k+u57uCqx8QRSQQ== dependencies: callsites "^3.0.0" graceful-fs "^4.2.3" source-map "^0.6.0" -"@jest/test-result@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.2.4.tgz#8fc9eac58e82eb2a82e4058e68c3814f98f59cf5" - integrity sha512-AI7eUy+q2lVhFnaibDFg68NGkrxVWZdD6KBr9Hm6EvN0oAe7GxpEwEavgPfNHQjU2mi6g+NsFn/6QPgTUwM1qg== +"@jest/test-result@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.3.0.tgz#137fab5e5c6fed36e5d40735d1eb029325e3bf06" + integrity sha512-mqrGuiiPXl1ap09Mydg4O782F3ouDQfsKqtQzIjitpwv3t1cHDwCto21jThw6WRRE+dKcWQvLG70GpyLJICfGw== dependencies: - "@jest/console" "^25.2.3" - "@jest/transform" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/console" "^25.3.0" + "@jest/types" "^25.3.0" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.2.4.tgz#28364aeddec140c696324114f63570f3de536c87" - integrity sha512-TEZm/Rkd6YgskdpTJdYLBtu6Gc11tfWPuSpatq0duH77ekjU8dpqX2zkPdY/ayuHxztV5LTJoV5BLtI9mZfXew== +"@jest/test-sequencer@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.3.0.tgz#271ad5f2b8f8137d092ccedc87e16a50f8676209" + integrity sha512-Xvns3xbji7JCvVcDGvqJ/pf4IpmohPODumoPEZJ0/VgC5gI4XaNVIBET2Dq5Czu6Gk3xFcmhtthh/MBOTljdNg== dependencies: - "@jest/test-result" "^25.2.4" - jest-haste-map "^25.2.3" - jest-runner "^25.2.4" - jest-runtime "^25.2.4" + "@jest/test-result" "^25.3.0" + jest-haste-map "^25.3.0" + jest-runner "^25.3.0" + jest-runtime "^25.3.0" -"@jest/transform@^25.2.4": - version "25.2.4" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.2.4.tgz#34336f37f13f62f7d1f5b93d5d150ba9eb3e11b9" - integrity sha512-6eRigvb+G6bs4kW5j1/y8wu4nCrmVuIe0epPBbiWaYlwawJ8yi1EIyK3d/btDqmBpN5GpN4YhR6iPPnDmkYdTA== +"@jest/transform@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.3.0.tgz#083c5447d5307d9b9494d6968115b647460e71f1" + integrity sha512-W01p8kTDvvEX6kd0tJc7Y5VdYyFaKwNWy1HQz6Jqlhu48z/8Gxp+yFCDVj+H8Rc7ezl3Mg0hDaGuFVkmHOqirg== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" babel-plugin-istanbul "^6.0.0" chalk "^3.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.3" - jest-haste-map "^25.2.3" - jest-regex-util "^25.2.1" - jest-util "^25.2.3" + jest-haste-map "^25.3.0" + jest-regex-util "^25.2.6" + jest-util "^25.3.0" micromatch "^4.0.2" pirates "^4.0.1" realpath-native "^2.0.0" @@ -858,25 +892,56 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^25.1.0": - version "25.1.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" - integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== +"@jest/types@^25.3.0": + version "25.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.3.0.tgz#88f94b277a1d028fd7117bc1f74451e0fc2131e7" + integrity sha512-UkaDNewdqXAmCDbN2GlUM6amDKS78eCqiw/UmF5nE0mmLTd6moJkiZJML/X52Ke3LH7Swhw883IRXq8o9nWjVw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^25.2.3": - version "25.2.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.2.3.tgz#035c4fb94e2da472f359ff9a211915d59987f6b6" - integrity sha512-6oLQwO9mKif3Uph3RX5J1i3S7X7xtDHWBaaaoeKw8hOzV6YUd0qDcYcHZ6QXMHDIzSr7zzrEa51o2Ovlj6AtKQ== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" +"@microsoft/api-documenter@^7.7.18": + version "7.7.18" + resolved "https://registry.yarnpkg.com/@microsoft/api-documenter/-/api-documenter-7.7.18.tgz#ad5b279ab9b416ba649f0811fad2a821dc3c60fa" + integrity sha512-mNR85fEt88HV7oFhrgEJ8JoTons6UX85XIDaMQ3fmq1xIx002XySnM6um7h2bUf1n8fLPcS9H3ISFEvvGW1qgA== + dependencies: + "@microsoft/api-extractor-model" "7.7.11" + "@microsoft/tsdoc" "0.12.19" + "@rushstack/node-core-library" "3.19.7" + "@rushstack/ts-command-line" "4.3.14" + colors "~1.2.1" + js-yaml "~3.13.1" + resolve "1.8.1" + +"@microsoft/api-extractor-model@7.7.11": + version "7.7.11" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.7.11.tgz#9dfc5425f4a6a2b6b1ebc39332ae8101ab8da36a" + integrity sha512-Kf3RytYDq7sP4ASaaA9IcvaOPbVj1Xj34E2Wxd9DznI7sG4HzcpoOGmxaZHCzyYVh7wfAaAlvcXf3SV+djhNZw== + dependencies: + "@microsoft/tsdoc" "0.12.19" + "@rushstack/node-core-library" "3.19.7" + +"@microsoft/api-extractor@^7.7.13": + version "7.7.13" + resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.7.13.tgz#7ee70978825a36d294d26e50b5d888ca3789d1b0" + integrity sha512-7+EGunGd7OSfDxZl4aDI4qMWG98+I84vTryNadti+IfgpB9IyhJtgA4r3dCG7hewTwKACBhi5aPqpH2C7l/30A== + dependencies: + "@microsoft/api-extractor-model" "7.7.11" + "@microsoft/tsdoc" "0.12.19" + "@rushstack/node-core-library" "3.19.7" + "@rushstack/ts-command-line" "4.3.14" + colors "~1.2.1" + lodash "~4.17.15" + resolve "1.8.1" + source-map "~0.6.1" + typescript "~3.7.2" + +"@microsoft/tsdoc@0.12.19": + version "0.12.19" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.12.19.tgz#2173ccb92469aaf62031fa9499d21b16d07f9b57" + integrity sha512-IpgPxHrNxZiMNUSXqR1l/gePKPkfAmIKoDRP9hp7OwjU29ZR8WCJsOJ8iBKgw0Qk+pFwR+8Y1cy8ImLY6e9m4A== "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -935,6 +1000,28 @@ dependencies: estree-walker "^1.0.1" +"@rushstack/node-core-library@3.19.7": + version "3.19.7" + resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.19.7.tgz#8d8a193fd6f99536c92dd797ab50fd5fcb7630ea" + integrity sha512-gKE/OXH5GAj8yJ1kEyRW68UekJernilZ3QTRgmQ0MUHBCQmtZ9Q6T5PQ1sVbcL4teH8BMdpZeFy1DKnHs8h3PA== + dependencies: + "@types/node" "10.17.13" + colors "~1.2.1" + fs-extra "~7.0.1" + jju "~1.4.0" + semver "~5.3.0" + timsort "~0.3.0" + z-schema "~3.18.3" + +"@rushstack/ts-command-line@4.3.14": + version "4.3.14" + resolved "https://registry.yarnpkg.com/@rushstack/ts-command-line/-/ts-command-line-4.3.14.tgz#5d7a437d4e9c564ff1b8e876215fca96c74858a1" + integrity sha512-YJZIyKvkm3f6ZdKSfMntHS9542Y2mmMWzaiPPoXxLFZntKxEIDE3WfUNlvOSo3yK4fNd09Tz3hfvTivQNHSiKQ== + dependencies: + "@types/argparse" "1.0.33" + argparse "~1.0.9" + colors "~1.2.1" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -954,10 +1041,15 @@ dependencies: defer-to-connect "^1.0.1" -"@types/babel__core@^7.1.0": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" - integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA== +"@types/argparse@1.0.33": + version "1.0.33" + resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.33.tgz#2728669427cdd74a99e53c9f457ca2866a37c52d" + integrity sha512-VQgHxyPMTj3hIlq9SY1mctqx+Jj8kpQfoLvDlVSDNOyuYs8JYfkuY3OW/4+dO657yPmNhHpePRx0/Tje5ImNVQ== + +"@types/babel__core@^7.1.7": + version "7.1.7" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" + integrity sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -1031,13 +1123,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@^25.1.4": - version "25.1.4" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.1.4.tgz#9e9f1e59dda86d3fd56afce71d1ea1b331f6f760" - integrity sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw== +"@types/jest@^25.2.1": + version "25.2.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5" + integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA== dependencies: - jest-diff "^25.1.0" - pretty-format "^25.1.0" + jest-diff "^25.2.1" + pretty-format "^25.2.1" "@types/minimatch@*": version "3.0.3" @@ -1049,6 +1141,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== +"@types/node@10.17.13": + version "10.17.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" + integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== + "@types/prettier@^1.19.0": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" @@ -1667,7 +1764,7 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -argparse@^1.0.7: +argparse@^1.0.7, argparse@~1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== @@ -1837,16 +1934,16 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== -babel-jest@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.2.4.tgz#b21b68d3af8f161c3e6e501e91f0dea8e652e344" - integrity sha512-+yDzlyJVWrqih9i2Cvjpt7COaN8vUwCsKGtxJLzg6I0xhxD54K8mvDUCliPKLufyzHh/c5C4MRj4Vk7VMjOjIg== +babel-jest@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.3.0.tgz#999d0c19e8427f66b796bf9ea233eedf087b957c" + integrity sha512-qiXeX1Cmw4JZ5yQ4H57WpkO0MZ61Qj+YnsVUwAMnDV5ls+yHon11XjarDdgP7H8lTmiEi6biiZA8y3Tmvx6pCg== dependencies: - "@jest/transform" "^25.2.4" - "@jest/types" "^25.2.3" - "@types/babel__core" "^7.1.0" + "@jest/transform" "^25.3.0" + "@jest/types" "^25.3.0" + "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^25.2.1" + babel-preset-jest "^25.3.0" chalk "^3.0.0" slash "^3.0.0" @@ -1878,10 +1975,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.2.1.tgz#d0003a1f3d5caa281e1107fe03bbf16b799f9955" - integrity sha512-HysbCQfJhxLlyxDbKcB2ucGYV0LjqK4h6dBoI3RtFuOxTiTWK6XGZMsHb0tGh8iJdV4hC6Z2GCHzVvDeh9i0lQ== +babel-plugin-jest-hoist@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.2.6.tgz#2af07632b8ac7aad7d414c1e58425d5fc8e84909" + integrity sha512-qE2xjMathybYxjiGFJg0mLFrz0qNp83aNZycWDY/SuHiZNq+vQfRQtuINqyXyue1ELd8Rd+1OhFSLjms8msMbw== dependencies: "@types/babel__traverse" "^7.0.6" @@ -1896,14 +1993,29 @@ babel-plugin-module-resolver@3.2.0: reselect "^3.0.1" resolve "^1.4.0" -babel-preset-jest@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.2.1.tgz#4ccd0e577f69aa11b71806edfe8b25a5c3ac93a2" - integrity sha512-zXHJBM5iR8oEO4cvdF83AQqqJf3tJrXy3x8nfu2Nlqvn4cneg4Ca8M7cQvC5S9BzDDy1O0tZ9iXru9J6E3ym+A== +babel-preset-current-node-syntax@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz#fb4a4c51fe38ca60fede1dc74ab35eb843cb41d6" + integrity sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +babel-preset-jest@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.3.0.tgz#9ab40aee52a19bdc52b8b1ec2403d5914ac3d86b" + integrity sha512-tjdvLKNMwDI9r+QWz9sZUQGTq1dpoxjUqFUpEasAc7MOtHg9XuLT2fx0udFG+k1nvMV0WvHHVAN7VmCZ+1Zxbw== dependencies: - "@babel/plugin-syntax-bigint" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - babel-plugin-jest-hoist "^25.2.1" + babel-plugin-jest-hoist "^25.2.6" + babel-preset-current-node-syntax "^0.1.2" balanced-match@^1.0.0: version "1.0.0" @@ -2395,6 +2507,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" + integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2583,6 +2703,11 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" +colors@~1.2.1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc" + integrity sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2595,7 +2720,7 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.20.0, commander@~2.20.3: +commander@^2.20.0, commander@^2.7.1, commander@~2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3441,15 +3566,10 @@ diacritics@^1.3.0: resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" integrity sha1-PvqHMj67hj5mls67AILUj/PW96E= -diff-sequences@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" - integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== - -diff-sequences@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.1.tgz#fcfe8aa07dd9b0c648396a478dabca8e76c6ab27" - integrity sha512-foe7dXnGlSh3jR1ovJmdv+77VQj98eKCHHwJPbZ2eEf0fHwKbkZicpPxEch9smZ+n2dnF6QFwkOQdLq9hpeJUg== +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== diffie-hellman@^5.0.0: version "5.0.3" @@ -3668,10 +3788,10 @@ enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.4.tgz#c608f2e1134c7f68c1c9ee056de13f9b31076de9" - integrity sha512-pkYrrDZumL2VS6VBGDhqbajCM2xpkUNLuKfGPjfKaSIBKYopQbqEFyrOkRMIb2HDR/rO1kGhEt/5twBwtzKBXw== +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== dependencies: ansi-colors "^3.2.1" @@ -3907,17 +4027,17 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/expect/-/expect-25.2.4.tgz#b66e0777c861034ebc21730bb34e1839d5d46806" - integrity sha512-hfuPhPds4yOsZtIw4kwAg70r0hqGmpqekgA+VX7pf/3wZ6FY+xIOXZhNsPMMMsspYG/YIsbAiwqsdnD4Ht+bCA== +expect@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-25.3.0.tgz#5fd36e51befd05afb7184bc954f8a4792d184c71" + integrity sha512-buboTXML2h/L0Kh44Ys2Cx49mX20ISc5KDirkxIs3Q9AJv0kazweUAbukegr+nHDOvFRKmxdojjIHCjqAceYfg== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" ansi-styles "^4.0.0" - jest-get-type "^25.2.1" - jest-matcher-utils "^25.2.3" - jest-message-util "^25.2.4" - jest-regex-util "^25.2.1" + jest-get-type "^25.2.6" + jest-matcher-utils "^25.3.0" + jest-message-util "^25.3.0" + jest-regex-util "^25.2.6" express@^4.17.1: version "4.17.1" @@ -4231,7 +4351,7 @@ fs-extra@8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^7.0.1: +fs-extra@^7.0.1, fs-extra@~7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== @@ -5398,10 +5518,10 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.0.tgz#d4d16d035db99581b6194e119bbf36c963c5eb70" - integrity sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A== +istanbul-reports@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -5416,142 +5536,127 @@ javascript-stringify@^2.0.1: resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.0.1.tgz#6ef358035310e35d667c675ed63d3eb7c1aa19e5" integrity sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow== -jest-changed-files@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.2.3.tgz#ad19deef9e47ba37efb432d2c9a67dfd97cc78af" - integrity sha512-EFxy94dvvbqRB36ezIPLKJ4fDIC+jAdNs8i8uTwFpaXd6H3LVc3ova1lNS4ZPWk09OCR2vq5kSdSQgar7zMORg== +jest-changed-files@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.3.0.tgz#85d8de6f4bd13dafda9d7f1e3f2565fc0e183c78" + integrity sha512-eqd5hyLbUjIVvLlJ3vQ/MoPxsxfESVXG9gvU19XXjKzxr+dXmZIqCXiY0OiYaibwlHZBJl2Vebkc0ADEMzCXew== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" execa "^3.2.0" throat "^5.0.0" -jest-cli@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.2.4.tgz#021c2383904696597abc060dcb133c82ebd8bfcc" - integrity sha512-zeY2pRDWKj2LZudIncvvguwLMEdcnJqc2jJbwza1beqi80qqLvkPF/BjbFkK2sIV3r+mfTJS+7ITrvK6pCdRjg== +jest-cli@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.3.0.tgz#d9e11f5700cc5946583cf0d01a9bdebceed448d2" + integrity sha512-XpNQPlW1tzpP7RGG8dxpkRegYDuLjzSiENu92+CYM87nEbmEPb3b4+yo8xcsHOnj0AG7DUt9b3uG8LuHI3MDzw== dependencies: - "@jest/core" "^25.2.4" - "@jest/test-result" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/core" "^25.3.0" + "@jest/test-result" "^25.3.0" + "@jest/types" "^25.3.0" chalk "^3.0.0" exit "^0.1.2" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^25.2.4" - jest-util "^25.2.3" - jest-validate "^25.2.3" + jest-config "^25.3.0" + jest-util "^25.3.0" + jest-validate "^25.3.0" prompts "^2.0.1" realpath-native "^2.0.0" yargs "^15.3.1" -jest-config@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.2.4.tgz#f4f33238979f225683179c89d1e402893008975d" - integrity sha512-fxy3nIpwJqOUQJRVF/q+pNQb6dv5b9YufOeCbpPZJ/md1zXpiupbhfehpfODhnKOfqbzSiigtSLzlWWmbRxnqQ== +jest-config@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.3.0.tgz#112b5e2f2e57dec4501dd2fe979044c06fb1317e" + integrity sha512-CmF1JnNWFmoCSPC4tnU52wnVBpuxHjilA40qH/03IHxIevkjUInSMwaDeE6ACfxMPTLidBGBCO3EbxvzPbo8wA== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^25.2.4" - "@jest/types" "^25.2.3" - babel-jest "^25.2.4" + "@jest/test-sequencer" "^25.3.0" + "@jest/types" "^25.3.0" + babel-jest "^25.3.0" chalk "^3.0.0" deepmerge "^4.2.2" glob "^7.1.1" - jest-environment-jsdom "^25.2.4" - jest-environment-node "^25.2.4" - jest-get-type "^25.2.1" - jest-jasmine2 "^25.2.4" - jest-regex-util "^25.2.1" - jest-resolve "^25.2.3" - jest-util "^25.2.3" - jest-validate "^25.2.3" + jest-environment-jsdom "^25.3.0" + jest-environment-node "^25.3.0" + jest-get-type "^25.2.6" + jest-jasmine2 "^25.3.0" + jest-regex-util "^25.2.6" + jest-resolve "^25.3.0" + jest-util "^25.3.0" + jest-validate "^25.3.0" micromatch "^4.0.2" - pretty-format "^25.2.3" + pretty-format "^25.3.0" realpath-native "^2.0.0" -jest-diff@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" - integrity sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw== - dependencies: - chalk "^3.0.0" - diff-sequences "^25.1.0" - jest-get-type "^25.1.0" - pretty-format "^25.1.0" - -jest-diff@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.2.3.tgz#54d601a0a754ef26e808a8c8dbadd278c215aa3f" - integrity sha512-VtZ6LAQtaQpFsmEzps15dQc5ELbJxy4L2DOSo2Ev411TUEtnJPkAMD7JneVypeMJQ1y3hgxN9Ao13n15FAnavg== +jest-diff@^25.2.1, jest-diff@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.3.0.tgz#0d7d6f5d6171e5dacde9e05be47b3615e147c26f" + integrity sha512-vyvs6RPoVdiwARwY4kqFWd4PirPLm2dmmkNzKqo38uZOzJvLee87yzDjIZLmY1SjM3XR5DwsUH+cdQ12vgqi1w== dependencies: chalk "^3.0.0" - diff-sequences "^25.2.1" - jest-get-type "^25.2.1" - pretty-format "^25.2.3" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.3.0" -jest-docblock@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.2.3.tgz#ac45280c43d59e7139f9fbe5896c6e0320c01ebb" - integrity sha512-d3/tmjLLrH5fpRGmIm3oFa3vOaD/IjPxtXVOrfujpfJ9y1tCDB1x/tvunmdOVAyF03/xeMwburl6ITbiQT1mVA== +jest-docblock@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.3.0.tgz#8b777a27e3477cd77a168c05290c471a575623ef" + integrity sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg== dependencies: detect-newline "^3.0.0" -jest-each@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.2.3.tgz#64067ba1508ebbd07e9b126c173ab371e8e6309d" - integrity sha512-RTlmCjsBDK2c9T5oO4MqccA3/5Y8BUtiEy7OOQik1iyCgdnNdHbI0pNEpyapZPBG0nlvZ4mIu7aY6zNUvLraAQ== +jest-each@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.3.0.tgz#a319eecf1f6076164ab86f99ca166a55b96c0bd4" + integrity sha512-aBfS4VOf/Qs95yUlX6d6WBv0szvOcTkTTyCIaLuQGj4bSHsT+Wd9dDngVHrCe5uytxpN8VM+NAloI6nbPjXfXw== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" chalk "^3.0.0" - jest-get-type "^25.2.1" - jest-util "^25.2.3" - pretty-format "^25.2.3" - -jest-environment-jsdom@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.2.4.tgz#f2783541d0538b1bc43641703372cea6a2e83611" - integrity sha512-5dm+tNwrLmhELdjAwiQnVGf/U9iFMWdTL4/wyrMg2HU6RQnCiuxpWbIigLHUhuP1P2Ak0F4k3xhjrikboKyShA== - dependencies: - "@jest/environment" "^25.2.4" - "@jest/fake-timers" "^25.2.4" - "@jest/types" "^25.2.3" - jest-mock "^25.2.3" - jest-util "^25.2.3" + jest-get-type "^25.2.6" + jest-util "^25.3.0" + pretty-format "^25.3.0" + +jest-environment-jsdom@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.3.0.tgz#c493ab8c41f28001520c70ef67dd88b88be6af05" + integrity sha512-jdE4bQN+k2QEZ9sWOxsqDJvMzbdFSCN/4tw8X0TQaCqyzKz58PyEf41oIr4WO7ERdp7WaJGBSUKF7imR3UW1lg== + dependencies: + "@jest/environment" "^25.3.0" + "@jest/fake-timers" "^25.3.0" + "@jest/types" "^25.3.0" + jest-mock "^25.3.0" + jest-util "^25.3.0" jsdom "^15.2.1" -jest-environment-node@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.2.4.tgz#dc211dfb0d8b66dfc1965a8f846e72e54ff0c430" - integrity sha512-Jkc5Y8goyXPrLRHnrUlqC7P4o5zn2m4zw6qWoRJ59kxV1f2a5wK+TTGhrhCwnhW/Ckpdl/pm+LufdvhJkvJbiw== - dependencies: - "@jest/environment" "^25.2.4" - "@jest/fake-timers" "^25.2.4" - "@jest/types" "^25.2.3" - jest-mock "^25.2.3" - jest-util "^25.2.3" +jest-environment-node@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.3.0.tgz#9845f0e63991e8498448cb0ae804935689533db9" + integrity sha512-XO09S29Nx1NU7TiMPHMoDIkxoGBuKSTbE+sHp0gXbeLDXhIdhysUI25kOqFFSD9AuDgvPvxWCXrvNqiFsOH33g== + dependencies: + "@jest/environment" "^25.3.0" + "@jest/fake-timers" "^25.3.0" + "@jest/types" "^25.3.0" + jest-mock "^25.3.0" + jest-util "^25.3.0" semver "^6.3.0" -jest-get-type@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" - integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== -jest-get-type@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.1.tgz#6c83de603c41b1627e6964da2f5454e6aa3c13a6" - integrity sha512-EYjTiqcDTCRJDcSNKbLTwn/LcDPEE7ITk8yRMNAOjEsN6yp+Uu+V1gx4djwnuj/DvWg0YGmqaBqPVGsPxlvE7w== - -jest-haste-map@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.2.3.tgz#2649392b5af191f0167a27bfb62e5d96d7eaaade" - integrity sha512-pAP22OHtPr4qgZlJJFks2LLgoQUr4XtM1a+F5UaPIZNiCRnePA0hM3L7aiJ0gzwiNIYwMTfKRwG/S1L28J3A3A== +jest-haste-map@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.3.0.tgz#b7683031c9c9ddc0521d311564108b244b11e4c6" + integrity sha512-LjXaRa+F8wwtSxo9G+hHD/Cp63PPQzvaBL9XCVoJD2rrcJO0Zr2+YYzAFWWYJ5GlPUkoaJFJtOuk0sL6MJY80A== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.3" - jest-serializer "^25.2.1" - jest-util "^25.2.3" - jest-worker "^25.2.1" + jest-serializer "^25.2.6" + jest-util "^25.3.0" + jest-worker "^25.2.6" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" @@ -5559,212 +5664,211 @@ jest-haste-map@^25.2.3: optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.2.4.tgz#5f77de83e1027f0c7588137055a80da773872374" - integrity sha512-juoKrmNmLwaheNbAg71SuUF9ovwUZCFNTpKVhvCXWk+SSeORcIUMptKdPCoLXV3D16htzhTSKmNxnxSk4SrTjA== +jest-jasmine2@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.3.0.tgz#16ae4f68adef65fb45001b26c864bcbcbf972830" + integrity sha512-NCYOGE6+HNzYFSui52SefgpsnIzvxjn6KAgqw66BdRp37xpMD/4kujDHLNW5bS5i53os5TcMn6jYrzQRO8VPrQ== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^25.2.4" - "@jest/source-map" "^25.2.1" - "@jest/test-result" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/environment" "^25.3.0" + "@jest/source-map" "^25.2.6" + "@jest/test-result" "^25.3.0" + "@jest/types" "^25.3.0" chalk "^3.0.0" co "^4.6.0" - expect "^25.2.4" + expect "^25.3.0" is-generator-fn "^2.0.0" - jest-each "^25.2.3" - jest-matcher-utils "^25.2.3" - jest-message-util "^25.2.4" - jest-runtime "^25.2.4" - jest-snapshot "^25.2.4" - jest-util "^25.2.3" - pretty-format "^25.2.3" + jest-each "^25.3.0" + jest-matcher-utils "^25.3.0" + jest-message-util "^25.3.0" + jest-runtime "^25.3.0" + jest-snapshot "^25.3.0" + jest-util "^25.3.0" + pretty-format "^25.3.0" throat "^5.0.0" -jest-leak-detector@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.2.3.tgz#4cf39f137925e0061c04c24ca65cae36465f0238" - integrity sha512-yblCMPE7NJKl7778Cf/73yyFWAas5St0iiEBwq7RDyaz6Xd4WPFnPz2j7yDb/Qce71A1IbDoLADlcwD8zT74Aw== +jest-leak-detector@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.3.0.tgz#5b6bf04903b35be56038915a55f47291771f769f" + integrity sha512-jk7k24dMIfk8LUSQQGN8PyOy9+J0NAfHZWiDmUDYVMctY8FLJQ1eQ8+PjMoN8PgwhLIggUqgYJnyRFvUz3jLRw== dependencies: - jest-get-type "^25.2.1" - pretty-format "^25.2.3" + jest-get-type "^25.2.6" + pretty-format "^25.3.0" -jest-matcher-utils@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.2.3.tgz#59285bd6d6c810debc9caa585ed985e46a3f28fd" - integrity sha512-ZmiXiwQRVM9MoKjGMP5YsGGk2Th5ncyRxfXKz5AKsmU8m43kgNZirckVzaP61MlSa9LKmXbevdYqVp1ZKAw2Rw== +jest-matcher-utils@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.3.0.tgz#76765788a26edaa8bc5f0100aea52ae383559648" + integrity sha512-ZBUJ2fchNIZt+fyzkuCFBb8SKaU//Rln45augfUtbHaGyVxCO++ANARdBK9oPGXU3hEDgyy7UHnOP/qNOJXFUg== dependencies: chalk "^3.0.0" - jest-diff "^25.2.3" - jest-get-type "^25.2.1" - pretty-format "^25.2.3" + jest-diff "^25.3.0" + jest-get-type "^25.2.6" + pretty-format "^25.3.0" -jest-message-util@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.2.4.tgz#b1441b9c82f5c11fc661303cbf200a2f136a7762" - integrity sha512-9wWMH3Bf+GVTv0GcQLmH/FRr0x0toptKw9TA8U5YFLVXx7Tq9pvcNzTyJrcTJ+wLqNbMPPJlJNft4MnlcrtF5Q== +jest-message-util@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.3.0.tgz#e3836826fe5ca538a337b87d9bd2648190867f85" + integrity sha512-5QNy9Id4WxJbRITEbA1T1kem9bk7y2fD0updZMSTNHtbEDnYOGLDPAuFBhFgVmOZpv0n6OMdVkK+WhyXEPCcOw== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/test-result" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" "@types/stack-utils" "^1.0.1" chalk "^3.0.0" micromatch "^4.0.2" slash "^3.0.0" stack-utils "^1.0.1" -jest-mock@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.2.3.tgz#b37a581f59d61bd91db27a99bf7eb8b3e5e993d5" - integrity sha512-xlf+pyY0j47zoCs8zGGOGfWyxxLximE8YFOfEK8s4FruR8DtM/UjNj61um+iDuMAFEBDe1bhCXkqiKoCmWjJzg== +jest-mock@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.3.0.tgz#d72644509e40987a732a9a2534a1054f4649402c" + integrity sha512-yRn6GbuqB4j3aYu+Z1ezwRiZfp0o9om5uOcBovVtkcRLeBCNP5mT0ysdenUsxAHnQUgGwPOE1wwhtQYe6NKirQ== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" jest-pnp-resolver@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== -jest-regex-util@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.2.1.tgz#db64b0d15cd3642c93b7b9627801d7c518600584" - integrity sha512-wroFVJw62LdqTdkL508ZLV82FrJJWVJMIuYG7q4Uunl1WAPTf4ftPKrqqfec4SvOIlvRZUdEX2TFpWR356YG/w== +jest-regex-util@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.2.6.tgz#d847d38ba15d2118d3b06390056028d0f2fd3964" + integrity sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw== -jest-resolve-dependencies@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.4.tgz#2d904400387d74a366dff54badb40a2b3210e733" - integrity sha512-qhUnK4PfNHzNdca7Ub1mbAqE0j5WNyMTwxBZZJjQlUrdqsiYho/QGK65FuBkZuSoYtKIIqriR9TpGrPEc3P5Gg== +jest-resolve-dependencies@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.3.0.tgz#b0e4ae053dd44ddacc18c6ee12b5b7c28e445a90" + integrity sha512-bDUlLYmHW+f7J7KgcY2lkq8EMRqKonRl0XoD4Wp5SJkgAxKJnsaIOlrrVNTfXYf+YOu3VCjm/Ac2hPF2nfsCIA== dependencies: - "@jest/types" "^25.2.3" - jest-regex-util "^25.2.1" - jest-snapshot "^25.2.4" + "@jest/types" "^25.3.0" + jest-regex-util "^25.2.6" + jest-snapshot "^25.3.0" -jest-resolve@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.2.3.tgz#ababeaf2bb948cb6d2dea8453759116da0fb7842" - integrity sha512-1vZMsvM/DBH258PnpUNSXIgtzpYz+vCVCj9+fcy4akZl4oKbD+9hZSlfe9RIDpU0Fc28ozHQrmwX3EqFRRIHGg== +jest-resolve@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.3.0.tgz#cb90a5bbea54a02eccdbbf4126a819595dcf91d6" + integrity sha512-IHoQAAybulsJ+ZgWis+ekYKDAoFkVH5Nx/znpb41zRtpxj4fr2WNV9iDqavdSm8GIpMlsfZxbC/fV9DhW0q9VQ== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" browser-resolve "^1.11.3" chalk "^3.0.0" jest-pnp-resolver "^1.2.1" realpath-native "^2.0.0" resolve "^1.15.1" -jest-runner@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.2.4.tgz#d0daf7c56b4a83b6b675863d5cdcd502c960f9a1" - integrity sha512-5xaIfqqxck9Wg2CV4b9KmJtf/sWO7zWQx7O+34GCLGPzoPcVmB3mZtdrQI1/jS3Reqjru9ycLjgLHSf6XoxRqA== +jest-runner@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.3.0.tgz#673ef2ac79d2810eb6b2c1a3f82398375a3d1174" + integrity sha512-csDqSC9qGHYWDrzrElzEgFbteztFeZJmKhSgY5jlCIcN0+PhActzRNku0DA1Xa1HxGOb0/AfbP1EGJlP4fGPtA== dependencies: - "@jest/console" "^25.2.3" - "@jest/environment" "^25.2.4" - "@jest/test-result" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/console" "^25.3.0" + "@jest/environment" "^25.3.0" + "@jest/test-result" "^25.3.0" + "@jest/types" "^25.3.0" chalk "^3.0.0" exit "^0.1.2" graceful-fs "^4.2.3" - jest-config "^25.2.4" - jest-docblock "^25.2.3" - jest-haste-map "^25.2.3" - jest-jasmine2 "^25.2.4" - jest-leak-detector "^25.2.3" - jest-message-util "^25.2.4" - jest-resolve "^25.2.3" - jest-runtime "^25.2.4" - jest-util "^25.2.3" - jest-worker "^25.2.1" + jest-config "^25.3.0" + jest-docblock "^25.3.0" + jest-haste-map "^25.3.0" + jest-jasmine2 "^25.3.0" + jest-leak-detector "^25.3.0" + jest-message-util "^25.3.0" + jest-resolve "^25.3.0" + jest-runtime "^25.3.0" + jest-util "^25.3.0" + jest-worker "^25.2.6" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.2.4.tgz#c66a421e115944426b377a7fd331f6c0902cfa56" - integrity sha512-6ehOUizgIghN+aV5YSrDzTZ+zJ9omgEjJbTHj3Jqes5D52XHfhzT7cSfdREwkNjRytrR7mNwZ7pRauoyNLyJ8Q== - dependencies: - "@jest/console" "^25.2.3" - "@jest/environment" "^25.2.4" - "@jest/source-map" "^25.2.1" - "@jest/test-result" "^25.2.4" - "@jest/transform" "^25.2.4" - "@jest/types" "^25.2.3" +jest-runtime@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.3.0.tgz#af4d40dbcc590fa5de9910cb6a120a13d131050b" + integrity sha512-gn5KYB1wxXRM3nfw8fVpthFu60vxQUCr+ShGq41+ZBFF3DRHZRKj3HDWVAVB4iTNBj2y04QeAo5cZ/boYaPg0w== + dependencies: + "@jest/console" "^25.3.0" + "@jest/environment" "^25.3.0" + "@jest/source-map" "^25.2.6" + "@jest/test-result" "^25.3.0" + "@jest/transform" "^25.3.0" + "@jest/types" "^25.3.0" "@types/yargs" "^15.0.0" chalk "^3.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.3" - jest-config "^25.2.4" - jest-haste-map "^25.2.3" - jest-message-util "^25.2.4" - jest-mock "^25.2.3" - jest-regex-util "^25.2.1" - jest-resolve "^25.2.3" - jest-snapshot "^25.2.4" - jest-util "^25.2.3" - jest-validate "^25.2.3" + jest-config "^25.3.0" + jest-haste-map "^25.3.0" + jest-message-util "^25.3.0" + jest-mock "^25.3.0" + jest-regex-util "^25.2.6" + jest-resolve "^25.3.0" + jest-snapshot "^25.3.0" + jest-util "^25.3.0" + jest-validate "^25.3.0" realpath-native "^2.0.0" slash "^3.0.0" strip-bom "^4.0.0" yargs "^15.3.1" -jest-serializer@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.2.1.tgz#51727a5fc04256f461abe0fa024a022ba165877a" - integrity sha512-fibDi7M5ffx6c/P66IkvR4FKkjG5ldePAK1WlbNoaU4GZmIAkS9Le/frAwRUFEX0KdnisSPWf+b1RC5jU7EYJQ== +jest-serializer@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.2.6.tgz#3bb4cc14fe0d8358489dbbefbb8a4e708ce039b7" + integrity sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ== -jest-snapshot@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.2.4.tgz#08d4517579c864df4280bcc948ceea34327a4ded" - integrity sha512-nIwpW7FZCq5p0AE3Oyqyb6jL0ENJixXzJ5/CD/XRuOqp3gS5OM3O/k+NnTrniCXxPFV4ry6s9HNfiPQBi0wcoA== +jest-snapshot@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.3.0.tgz#d4feb457494f4aaedcc83fbbf1ca21808fc3df71" + integrity sha512-GGpR6Oro2htJPKh5RX4PR1xwo5jCEjtvSPLW1IS7N85y+2bWKbiknHpJJRKSdGXghElb5hWaeQASJI4IiRayGg== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" "@types/prettier" "^1.19.0" chalk "^3.0.0" - expect "^25.2.4" - jest-diff "^25.2.3" - jest-get-type "^25.2.1" - jest-matcher-utils "^25.2.3" - jest-message-util "^25.2.4" - jest-resolve "^25.2.3" + expect "^25.3.0" + jest-diff "^25.3.0" + jest-get-type "^25.2.6" + jest-matcher-utils "^25.3.0" + jest-message-util "^25.3.0" + jest-resolve "^25.3.0" make-dir "^3.0.0" natural-compare "^1.4.0" - pretty-format "^25.2.3" + pretty-format "^25.3.0" semver "^6.3.0" -jest-util@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.2.3.tgz#0abf95a1d6b96f2de5a3ecd61b36c40a182dc256" - integrity sha512-7tWiMICVSo9lNoObFtqLt9Ezt5exdFlWs5fLe1G4XLY2lEbZc814cw9t4YHScqBkWMfzth8ASHKlYBxiX2rdCw== +jest-util@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.3.0.tgz#e3b0064165818f10d78514696fd25efba82cf049" + integrity sha512-dc625P/KS/CpWTJJJxKc4bA3A6c+PJGBAqS8JTJqx4HqPoKNqXg/Ec8biL2Z1TabwK7E7Ilf0/ukSEXM1VwzNA== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" chalk "^3.0.0" is-ci "^2.0.0" make-dir "^3.0.0" -jest-validate@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.2.3.tgz#ecb0f093cf8ae71d15075fb48439b6f78f1fcb5a" - integrity sha512-GObn91jzU0B0Bv4cusAwjP6vnWy78hJUM8MOSz7keRfnac/ZhQWIsUjvk01IfeXNTemCwgR57EtdjQMzFZGREg== +jest-validate@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.3.0.tgz#eb95fdee0039647bcd5d4be641b21e4a142a880c" + integrity sha512-3WuXgIZ4HXUvW6gk9twFFkT9j6zUorKnF2oEY8VEsHb7x5LGvVlN3WUsbqazVKuyXwvikO2zFJ/YTySMsMje2w== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" camelcase "^5.3.1" chalk "^3.0.0" - jest-get-type "^25.2.1" + jest-get-type "^25.2.6" leven "^3.1.0" - pretty-format "^25.2.3" + pretty-format "^25.3.0" -jest-watcher@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.2.4.tgz#dda85b914d470fa4145164a8f70bda4f208bafb6" - integrity sha512-p7g7s3zqcy69slVzQYcphyzkB2FBmJwMbv6k6KjI5mqd6KnUnQPfQVKuVj2l+34EeuxnbXqnrjtUFmxhcL87rg== +jest-watcher@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.3.0.tgz#fd03fd5ca52f02bd3161ab177466bf1bfdd34e5c" + integrity sha512-dtFkfidFCS9Ucv8azOg2hkiY3sgJEHeTLtGFHS+jfBEE7eRtrO6+2r1BokyDkaG2FOD7485r/SgpC1MFAENfeA== dependencies: - "@jest/test-result" "^25.2.4" - "@jest/types" "^25.2.3" + "@jest/test-result" "^25.3.0" + "@jest/types" "^25.3.0" ansi-escapes "^4.2.1" chalk "^3.0.0" - jest-util "^25.2.3" + jest-util "^25.3.0" string-length "^3.1.0" jest-worker@^24.9.0: @@ -5775,22 +5879,27 @@ jest-worker@^24.9.0: merge-stream "^2.0.0" supports-color "^6.1.0" -jest-worker@^25.2.1: - version "25.2.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.2.1.tgz#209617015c768652646aa33a7828cc2ab472a18a" - integrity sha512-IHnpekk8H/hCUbBlfeaPZzU6v75bqwJp3n4dUrQuQOAgOneI4tx3jV2o8pvlXnDfcRsfkFIUD//HWXpCmR+evQ== +jest-worker@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.2.6.tgz#d1292625326794ce187c38f51109faced3846c58" + integrity sha512-FJn9XDUSxcOR4cwDzRfL1z56rUofNTFs539FGASpd50RHdb6EVkhxQqktodW2mI49l+W3H+tFJDotCHUQF6dmA== dependencies: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.2.4: - version "25.2.4" - resolved "https://registry.yarnpkg.com/jest/-/jest-25.2.4.tgz#d10941948a2b57eb7accc2e7ae78af4a0e11b40a" - integrity sha512-Lu4LXxf4+durzN/IFilcAoQSisOwgHIXgl9vffopePpSSwFqfj1Pj4y+k3nL8oTbnvjxgDIsEcepy6he4bWqnQ== +jest@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-25.3.0.tgz#7a5e59741d94b8662664c77a9f346246d6bf228b" + integrity sha512-iKd5ShQSHzFT5IL/6h5RZJhApgqXSoPxhp5HEi94v6OAw9QkF8T7X+liEU2eEHJ1eMFYTHmeWLrpBWulsDpaUg== dependencies: - "@jest/core" "^25.2.4" + "@jest/core" "^25.3.0" import-local "^3.0.2" - jest-cli "^25.2.4" + jest-cli "^25.3.0" + +jju@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" + integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo= js-levenshtein@^1.1.3: version "1.1.6" @@ -5802,7 +5911,7 @@ js-levenshtein@^1.1.3: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1, js-yaml@^3.11.0, js-yaml@^3.13.1: +js-yaml@3.13.1, js-yaml@^3.11.0, js-yaml@^3.13.1, js-yaml@~3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -6099,6 +6208,16 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.get@^4.0.0: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.isequal@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -6139,7 +6258,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5: +lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5, lodash@~4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -6443,6 +6562,14 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +micromatch@4.x, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -6462,14 +6589,6 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -7253,7 +7372,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.5, path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -7735,11 +7854,16 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@1.19.1, prettier@^1.18.2: +prettier@^1.18.2: version "1.19.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef" + integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w== + pretty-error@^2.0.2: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -7748,22 +7872,12 @@ pretty-error@^2.0.2: renderkid "^2.0.1" utila "~0.4" -pretty-format@^25.1.0: - version "25.1.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" - integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== - dependencies: - "@jest/types" "^25.1.0" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^16.12.0" - -pretty-format@^25.2.3: - version "25.2.3" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.3.tgz#ba6e9603a0d80fa2e470b1fed55de1f9bfd81421" - integrity sha512-IP4+5UOAVGoyqC/DiomOeHBUKN6q00gfyT2qpAsRH64tgOKB2yF7FHJXC18OCiU0/YFierACup/zdCOWw0F/0w== +pretty-format@^25.2.1, pretty-format@^25.3.0: + version "25.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.3.0.tgz#d0a4f988ff4a6cd350342fdabbb809aeb4d49ad5" + integrity sha512-wToHwF8bkQknIcFkBqNfKu4+UZqnrLn/Vr+wwKQwwvPzkBfDDKp/qIabFqdgtoi5PEnM8LFByVsOrHoa3SpTVA== dependencies: - "@jest/types" "^25.2.3" + "@jest/types" "^25.3.0" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^16.12.0" @@ -8298,6 +8412,13 @@ resolve@1.15.1, resolve@^1.14.2, resolve@^1.15.1: dependencies: path-parse "^1.0.6" +resolve@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== + dependencies: + path-parse "^1.0.5" + resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.2.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: version "1.15.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5" @@ -8383,10 +8504,10 @@ rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.3.2.tgz#afa68e4f3325bcef4e150d082056bef450bcac60" - integrity sha512-p66+fbfaUUOGE84sHXAOgfeaYQMslgAazoQMp//nlR519R61213EPFgrMZa48j31jNacJwexSAR1Q8V/BwGKBA== +rollup@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.3.4.tgz#979e461f8cab1a71aec2d37ce2a434052ae70223" + integrity sha512-8U9x54RCVhrUEV1zon4Pp8kokg1HM0fwzf5vkwe2/rOfyTClarx5e27kFlaoZ7ofJiazWkNQ+dgdG4HuZxkQ9A== optionalDependencies: fsevents "~2.1.2" @@ -8502,10 +8623,15 @@ semver@6.x, semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6" - integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA== +semver@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.2.1.tgz#d997aa36bdbb00b501ae4ac4c7d17e9f7a587ae5" + integrity sha512-aHhm1pD02jXXkyIpq25qBZjr3CQgg8KST8uX0OWXch3xE6jw+1bfbWnCjzMwojsTquroUmKFHNzU6x26mEiRxw== + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= send@0.17.1: version "0.17.1" @@ -9326,7 +9452,7 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -timsort@^0.3.0: +timsort@^0.3.0, timsort@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= @@ -9455,10 +9581,10 @@ trim-off-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= -ts-jest@^25.3.0: - version "25.3.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.3.0.tgz#c12d34573cbe34d49f10567940e44fd19d1c9178" - integrity sha512-qH/uhaC+AFDU9JfAueSr0epIFJkGMvUPog4FxSEVAtPOur1Oni5WBJMiQIkfHvc7PviVRsnlVLLY2I6221CQew== +ts-jest@^25.3.1: + version "25.3.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.3.1.tgz#58e2ed3506e4e4487c0b9b532846a5cade9656ba" + integrity sha512-O53FtKguoMUByalAJW+NWEv7c4tus5ckmhfa7/V0jBb2z8v5rDSLFC1Ate7wLknYPC1euuhY6eJjQq4FtOZrkg== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -9466,10 +9592,11 @@ ts-jest@^25.3.0: json5 "2.x" lodash.memoize "4.x" make-error "1.x" + micromatch "4.x" mkdirp "1.x" resolve "1.x" semver "6.x" - yargs-parser "^18.1.1" + yargs-parser "18.x" tslib@1.11.1: version "1.11.1" @@ -9540,6 +9667,11 @@ typescript@^3.8.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@~3.7.2: + version "3.7.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" + integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -9799,6 +9931,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" + integrity sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -10280,6 +10417,14 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yargs-parser@18.x, yargs-parser@^18.1.1: + version "18.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" + integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^10.0.0: version "10.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" @@ -10295,14 +10440,6 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.1: - version "18.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" - integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs@12.0.5: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" @@ -10338,6 +10475,17 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" +z-schema@~3.18.3: + version "3.18.4" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.4.tgz#ea8132b279533ee60be2485a02f7e3e42541a9a2" + integrity sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw== + dependencies: + lodash.get "^4.0.0" + lodash.isequal "^4.0.0" + validator "^8.0.0" + optionalDependencies: + commander "^2.7.1" + zepto@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98" From 57b2828c1e7a2b3ebfd0780c9269fc3838457ecc Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Thu, 9 Apr 2020 23:50:00 +0900 Subject: [PATCH 55/56] chore: fix clean script not working --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b75fdc53..765bad79 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "build": "node scripts/build.js", "build:dts": "api-extractor run --local --verbose", - "clean": "rm -rf dist && rm -rf dist && rm -rf coverage && && rm -rf temp && rm -rf .nyc_output && rm -rf .tmp", + "clean": "rm -rf dist && rm -rf dist && rm -rf coverage && rm -rf temp && rm -rf .nyc_output && rm -rf .tmp", "lint": "prettier --check --write --parser typescript \"{src,test}/**/*.ts\"", "lint:fail": "prettier --check --parser typescript \"{src,test}/**/*.ts\"", "test": "jest --testPathIgnorePatterns test/performance", From c28fdac5c05aaa48857e2213cfe207deab537648 Mon Sep 17 00:00:00 2001 From: Kia Ishii Date: Fri, 10 Apr 2020 03:04:12 +0900 Subject: [PATCH 56/56] feat: add belongs to relation --- src/index.ts | 2 + src/model/Model.ts | 128 +++++++++++++++--- src/model/attributes/relations/BelongsTo.ts | 104 ++++++++++++++ src/model/attributes/relations/HasMany.ts | 2 +- src/model/attributes/relations/HasOne.ts | 2 +- src/model/attributes/relations/Relation.ts | 2 +- src/model/attributes/types/Attr.ts | 2 +- .../attributes/relations/BelongsTo.ts | 17 +++ src/query/Query.ts | 26 ++-- src/repository/Repository.ts | 3 +- src/support/Utils.ts | 13 ++ test/Helpers.ts | 4 +- .../relations/types/belongs_to_insert.spec.ts | 94 +++++++++++++ .../types/belongs_to_retrieve.spec.ts | 67 +++++++++ .../relations/types/has_many_retrieve.spec.ts | 2 +- .../feature/repository/retrieves_find.spec.ts | 17 +-- 16 files changed, 438 insertions(+), 47 deletions(-) create mode 100644 src/model/attributes/relations/BelongsTo.ts create mode 100644 src/model/decorators/attributes/relations/BelongsTo.ts create mode 100644 test/feature/relations/types/belongs_to_insert.spec.ts create mode 100644 test/feature/relations/types/belongs_to_retrieve.spec.ts diff --git a/src/index.ts b/src/index.ts index e4c79481..48c3ed2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from './model/decorators/attributes/types/Str' export * from './model/decorators/attributes/types/Num' export * from './model/decorators/attributes/types/Bool' export * from './model/decorators/attributes/relations/HasOne' +export * from './model/decorators/attributes/relations/BelongsTo' export * from './model/decorators/attributes/relations/HasMany' export * from './model/decorators/Contracts' export * from './model/attributes/Attribute' @@ -20,6 +21,7 @@ export { Number as NumberAttr } from './model/attributes/types/Number' export { Boolean as BooleanAttr } from './model/attributes/types/Boolean' export * from './model/attributes/relations/Relation' export { HasOne as HasOneAttr } from './model/attributes/relations/HasOne' +export { BelongsTo as BelongsToAttr } from './model/attributes/relations/BelongsTo' export { HasMany as HasManyAttr } from './model/attributes/relations/HasMany' export * from './modules/RootModule' export * from './modules/RootState' diff --git a/src/model/Model.ts b/src/model/Model.ts index 137f6e7c..63e644cd 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,5 +1,6 @@ import { Store } from 'vuex' -import { Element } from '../data/Data' +import { isArray } from '../support/Utils' +import { Element, Item, Collection } from '../data/Data' import { Attribute } from './attributes/Attribute' import { Attr } from './attributes/types/Attr' import { String as Str } from './attributes/types/String' @@ -7,6 +8,7 @@ import { Number as Num } from './attributes/types/Number' import { Boolean as Bool } from './attributes/types/Boolean' import { Relation } from './attributes/relations/Relation' import { HasOne } from './attributes/relations/HasOne' +import { BelongsTo } from './attributes/relations/BelongsTo' import { HasMany } from './attributes/relations/HasMany' export type ModelFields = Record @@ -131,9 +133,26 @@ export class Model { foreignKey: string, localKey?: string ): HasOne { - localKey = localKey ?? this.getLocalKey() + const model = new this() - return new HasOne(new this(), new related(), foreignKey, localKey) + localKey = localKey ?? model.$getLocalKey() + + return new HasOne(model, new related(), foreignKey, localKey) + } + + /** + * Create a new belongs to relation instance. + */ + static belongsTo( + related: typeof Model, + foreignKey: string, + ownerKey?: string + ): BelongsTo { + const instance = new related() + + ownerKey = ownerKey ?? instance.$getLocalKey() + + return new BelongsTo(new this(), instance, foreignKey, ownerKey) } /** @@ -144,16 +163,11 @@ export class Model { foreignKey: string, localKey?: string ): HasMany { - localKey = localKey ?? this.getLocalKey() + const model = new this() - return new HasMany(new this(), new related(), foreignKey, localKey) - } + localKey = localKey ?? model.$getLocalKey() - /** - * Get the local key for the model. - */ - static getLocalKey(): string { - return this.primaryKey + return new HasMany(model, new related(), foreignKey, localKey) } /** @@ -201,7 +215,7 @@ export class Model { $newInstance(attributes?: Element, options?: ModelOptions): this { const model = new this.$self(attributes, options) as this - model.$setStore(model.$store) + model.$setStore(this.$store) return model } @@ -285,6 +299,13 @@ export class Model { return String(target[this.$primaryKey]) } + /** + * Get the local key for the model. + */ + $getLocalKey(): string { + return this.$primaryKey + } + /** * Check if the model has any relations defined in the schema. */ @@ -328,14 +349,89 @@ export class Model { * Get the serialized model attributes. */ $getAttributes(): Element { - const record = {} as Element + return this.$toJson(this, { relations: false }) + } - for (const key in this.$fields) { - if (this[key] !== undefined) { - record[key] = this[key] + /** + * Serialize given model POJO. + */ + $toJson(model?: Model, options: ModelOptions = {}): Element { + model = model ?? this + + const withRelation = options.relations ?? true + + const record: Element = {} + + for (const key in model.$fields) { + const attr = this.$fields[key] + const value = model[key] + + if (!(attr instanceof Relation)) { + record[key] = this.serializeValue(value) + continue + } + + if (withRelation) { + record[key] = this.serializeRelation(value) } } return record } + + /** + * Serialize given value. + */ + protected serializeValue(v: any): any { + if (v === null) { + return null + } + + if (isArray(v)) { + return this.serializeArray(v) + } + + if (typeof v === 'object') { + return this.serializeObject(v) + } + + return v + } + + /** + * Serialize an array into json. + */ + protected serializeArray(a: any[]): any[] { + return a.map((v) => this.serializeValue(v)) + } + + /** + * Serialize an object into json. + */ + protected serializeObject(o: object): object { + const obj = {} + + for (const key in o) { + obj[key] = this.serializeValue(o[key]) + } + + return obj + } + + /** + * Serialize given relation into json. + */ + protected serializeRelation(relation: Item): Element | null + protected serializeRelation(relation: Collection): Element[] + protected serializeRelation(relation: any): any { + if (relation === null) { + return null + } + + if (isArray(relation)) { + return relation.map((model) => model.$toJson()) + } + + return relation.$toJson() + } } diff --git a/src/model/attributes/relations/BelongsTo.ts b/src/model/attributes/relations/BelongsTo.ts new file mode 100644 index 00000000..9d708128 --- /dev/null +++ b/src/model/attributes/relations/BelongsTo.ts @@ -0,0 +1,104 @@ +import { Schema as NormalizrSchema } from 'normalizr' +import { Schema } from '../../../schema/Schema' +import { Element, NormalizedData, Collection } from '../../../data/Data' +import { Query } from '../../../query/Query' +import { Model } from '../../Model' +import { Relation } from './Relation' + +export class BelongsTo extends Relation { + /** + * The child model instance of the relation. + */ + protected child: Model + + /** + * The foreign key of the parent model. + */ + protected foreignKey: string + + /** + * The associated key on the parent model. + */ + protected ownerKey: string + + /** + * Create a new belongs to relation instance. + */ + constructor( + parent: Model, + child: Model, + foreignKey: string, + ownerKey: string + ) { + super(parent, child) + this.foreignKey = foreignKey + this.ownerKey = ownerKey + + // In the underlying base relationship class, this variable is referred to as + // the "parent" since most relationships are not inversed. But, since this + // one is we will create a "child" variable for much better readability. + this.child = child + } + + /** + * Define the normalizr schema for the relationship. + */ + define(schema: Schema): NormalizrSchema { + return schema.one(this.child) + } + + /** + * Attach the relational key to the given data. + */ + attach(id: string | number, record: Element, data: NormalizedData): void { + const childRecord = data[this.child.$entity]?.[id] + + if (childRecord) { + record[this.foreignKey] = childRecord[this.ownerKey] + } + } + + /** + * Set the constraints for an eager load of the relation. + */ + addEagerConstraints(query: Query, models: Collection): void { + query.whereIn(this.ownerKey, this.getEagerModelKeys(models)) + } + + /** + * Gather the keys from an array of related models. + */ + protected getEagerModelKeys(models: Collection): (string | number)[] { + return models.reduce<(string | number)[]>((keys, model) => { + if (model[this.foreignKey] !== null) { + keys.push(model[this.foreignKey]) + } + + return keys + }, []) + } + + /** + * Match the eagerly loaded results to their parents. + */ + match(relation: string, models: Collection, results: Collection): void { + const dictionary = results.reduce>((dic, result) => { + dic[result[this.ownerKey]] = result + + return dic + }, {}) + + models.forEach((model) => { + dictionary[model[this.foreignKey]] + ? model.$setRelation(relation, dictionary[model[this.foreignKey]]) + : model.$setRelation(relation, null) + }) + } + + /** + * Make the value for the attribute. + */ + make(_value: any): null { + return null + } +} diff --git a/src/model/attributes/relations/HasMany.ts b/src/model/attributes/relations/HasMany.ts index 4a080bd3..231ae648 100644 --- a/src/model/attributes/relations/HasMany.ts +++ b/src/model/attributes/relations/HasMany.ts @@ -67,7 +67,7 @@ export class HasMany extends Relation { match(relation: string, models: Collection, results: Collection): void { const dictionary = this.buildDictionary(results) - models.map((model) => { + models.forEach((model) => { const key = model[this.localKey] dictionary[key] diff --git a/src/model/attributes/relations/HasOne.ts b/src/model/attributes/relations/HasOne.ts index 8f0a076d..96734a89 100644 --- a/src/model/attributes/relations/HasOne.ts +++ b/src/model/attributes/relations/HasOne.ts @@ -61,7 +61,7 @@ export class HasOne extends Relation { match(relation: string, models: Collection, results: Collection): void { const dictionary = this.buildDictionary(results) - models.map((model) => { + models.forEach((model) => { const key = model[this.localKey] dictionary[key] diff --git a/src/model/attributes/relations/Relation.ts b/src/model/attributes/relations/Relation.ts index 0b7a4be2..330261c7 100644 --- a/src/model/attributes/relations/Relation.ts +++ b/src/model/attributes/relations/Relation.ts @@ -70,7 +70,7 @@ export abstract class Relation extends Attribute { /** * Get all of the primary keys for an array of models. */ - protected getKeys(models: Collection, key: string) { + protected getKeys(models: Collection, key: string): (string | number)[] { return models.map((model) => model[key]) } diff --git a/src/model/attributes/types/Attr.ts b/src/model/attributes/types/Attr.ts index 410dd154..15ce6b06 100644 --- a/src/model/attributes/types/Attr.ts +++ b/src/model/attributes/types/Attr.ts @@ -5,6 +5,6 @@ export class Attr extends Type { * Make the value for the attribute. */ make(value: any): any { - return value + return value === undefined ? this.value : value } } diff --git a/src/model/decorators/attributes/relations/BelongsTo.ts b/src/model/decorators/attributes/relations/BelongsTo.ts new file mode 100644 index 00000000..73883d1f --- /dev/null +++ b/src/model/decorators/attributes/relations/BelongsTo.ts @@ -0,0 +1,17 @@ +import { Model } from '../../../Model' +import { PropertyDecorator } from '../../Contracts' + +/** + * Create a belongs to decorator. + */ +export function BelongsTo( + related: () => typeof Model, + foreignKey: string, + ownerKey?: string +): PropertyDecorator { + return (target, propertyKey) => { + target.$self.setRegistry(propertyKey, () => + target.$self.belongsTo(related(), foreignKey, ownerKey) + ) + } +} diff --git a/src/query/Query.ts b/src/query/Query.ts index 648d592a..59c647c5 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -49,7 +49,7 @@ export class Query { * Create a new query instance from the given relation. */ protected newQueryForRelation(relation: Relation): Query { - return new Query(this.store, relation.getRelated()) + return new Query(this.store, relation.getRelated().$setStore(this.store)) } /** @@ -150,16 +150,16 @@ export class Query { return this.findIn(ids) } - const model = this.findRaw(ids) + const record = this.findRaw(ids) - return model ? this.hydrateElement(model) : null + return record ? this.hydrate(record) : null } /** * Find multiple models by their primary keys. */ findIn(ids: (string | number)[]): Collection { - return this.hydrateElements(this.findInRaw(ids)) + return this.hydrateMany(this.findInRaw(ids)) } /** @@ -190,7 +190,7 @@ export class Query { const collection = [] as Collection for (const id in records) { - collection.push(this.hydrateElement(records[id])) + collection.push(this.hydrate(records[id])) } return collection @@ -302,9 +302,9 @@ export class Query { * Insert the given record to the store. */ async insert(records: Element[]): Promise> { - const models = this.hydrateElements(records) + const models = this.hydrateMany(records) - this.connection().insert(this.dehydrateModels(models)) + this.connection().insert(this.dehydrate(models)) return models } @@ -315,7 +315,7 @@ export class Query { async merge(records: Element[]): Promise> { const models = this.getMergedModels(records) - this.connection().update(this.dehydrateModels(models)) + this.connection().update(this.dehydrate(models)) return models } @@ -340,7 +340,7 @@ export class Query { async update(record: Element): Promise> { const models = this.mergeModelsWithElement(this.get(), record) - this.connection().update(this.dehydrateModels(models)) + this.connection().update(this.dehydrate(models)) return models } @@ -413,21 +413,21 @@ export class Query { /** * Instantiate new models with the given record. */ - protected hydrateElement(record: Element): M { + protected hydrate(record: Element): M { return this.model.$newInstance(record, { relations: false }) } /** * Instantiate new models with the given collection of records. */ - protected hydrateElements(records: Element[]): Collection { - return records.map((record) => this.hydrateElement(record)) + protected hydrateMany(records: Element[]): Collection { + return records.map((record) => this.hydrate(record)) } /** * Convert all models into the plain record. */ - protected dehydrateModels(models: Collection): Element[] { + protected dehydrate(models: Collection): Element[] { return models.map((model) => model.$getAttributes()) } diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 7c190d9c..781ef885 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -35,7 +35,8 @@ export class Repository { */ constructor(store: Store, model: M | Constructor) { this.store = store - this.model = model instanceof Model ? model : new model() + + this.model = model instanceof Model ? model : new model().$setStore(store) } /** diff --git a/src/support/Utils.ts b/src/support/Utils.ts index 186f21b6..491ad5b5 100644 --- a/src/support/Utils.ts +++ b/src/support/Utils.ts @@ -4,10 +4,23 @@ interface SortableArray { value: T } +/** + * Check if the given value is the type of undefined or null. + */ +export function isNullish(value: any): value is undefined | null { + return value === undefined || value === null +} + +/** + * Check if the given value is the type of object. + */ export function isObject(value: any): value is Record { return value !== null && typeof value === 'object' } +/** + * Check if the given value is the type of array. + */ export function isArray(value: any): value is any[] { return Array.isArray(value) } diff --git a/test/Helpers.ts b/test/Helpers.ts index c204b2ee..fa02a018 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -54,7 +54,7 @@ export function assertState(store: Store, entities: Entities): void { } export function assertModel(model: M, record: Element): void { - expect(model.$getAttributes()).toEqual(record) + expect(model.$toJson()).toEqual(record) } export function assertModels( @@ -62,7 +62,7 @@ export function assertModels( record: Element[] ): void { models.forEach((model, index) => { - expect(model.$getAttributes()).toEqual(record[index]) + expect(model.$toJson()).toEqual(record[index]) }) } diff --git a/test/feature/relations/types/belongs_to_insert.spec.ts b/test/feature/relations/types/belongs_to_insert.spec.ts new file mode 100644 index 00000000..fb5a0895 --- /dev/null +++ b/test/feature/relations/types/belongs_to_insert.spec.ts @@ -0,0 +1,94 @@ +import { createStore, assertState } from 'test/Helpers' +import { Model, Attr, Str, BelongsTo } from '@/index' + +describe('feature/relations/types/belongs_to_insert', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + } + + class Post extends Model { + static entity = 'posts' + + @Attr() id!: number + @Attr() userId!: number | null + @Str('') title!: string + + @BelongsTo(() => User, 'userId') + author!: User | null + } + + it('inserts a record to the store with "belongs to" relation', async () => { + const store = createStore([User, Post]) + + await store.$repo(Post).insert({ + id: 1, + userId: 1, + title: 'Title 01', + author: { id: 1, name: 'John Doe' } + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' } + } + }) + }) + + it('generates missing foreign key', async () => { + const store = createStore([User, Post]) + + await store.$repo(Post).insert({ + id: 1, + title: 'Title 01', + author: { id: 1, name: 'John Doe' } + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' } + } + }) + }) + + it('can insert a record with missing relational key', async () => { + const store = createStore([User, Post]) + + await store.$repo(Post).insert({ + id: 1, + title: 'Title 01' + }) + + assertState(store, { + users: {}, + posts: { + 1: { id: 1, userId: null, title: 'Title 01' } + } + }) + }) + + it('can insert a record with relational key set to `null`', async () => { + const store = createStore([User, Post]) + + await store.$repo(Post).insert({ + id: 1, + title: 'Title 01', + author: null + }) + + assertState(store, { + users: {}, + posts: { + 1: { id: 1, userId: null, title: 'Title 01' } + } + }) + }) +}) diff --git a/test/feature/relations/types/belongs_to_retrieve.spec.ts b/test/feature/relations/types/belongs_to_retrieve.spec.ts new file mode 100644 index 00000000..95285cd3 --- /dev/null +++ b/test/feature/relations/types/belongs_to_retrieve.spec.ts @@ -0,0 +1,67 @@ +import { createStore, fillState, assertModel } from 'test/Helpers' +import { Model, Attr, Str, BelongsTo } from '@/index' + +describe('feature/relations/types/belongs_to_retrieve', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + } + + class Post extends Model { + static entity = 'posts' + + @Attr() id!: number + @Attr() userId!: number | null + @Str('') title!: string + + @BelongsTo(() => User, 'userId') + author!: User | null + } + + it('can eager load belongs to relation', async () => { + const store = createStore([User, Post]) + + fillState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' } + } + }) + + const post = store.$repo(Post).with('author').first()! + + expect(post).toBeInstanceOf(Post) + expect(post.author).toBeInstanceOf(User) + assertModel(post, { + id: 1, + userId: 1, + title: 'Title 01', + author: { id: 1, name: 'John Doe' } + }) + }) + + it('can eager load missing relation as `null`', async () => { + const store = createStore([User, Post]) + + fillState(store, { + users: {}, + posts: { + 1: { id: 1, userId: 1, title: 'Title 01' } + } + }) + + const post = store.$repo(Post).with('author').first()! + + expect(post).toBeInstanceOf(Post) + assertModel(post, { + id: 1, + userId: 1, + title: 'Title 01', + author: null + }) + }) +}) diff --git a/test/feature/relations/types/has_many_retrieve.spec.ts b/test/feature/relations/types/has_many_retrieve.spec.ts index 2ced3592..486cbe3c 100644 --- a/test/feature/relations/types/has_many_retrieve.spec.ts +++ b/test/feature/relations/types/has_many_retrieve.spec.ts @@ -25,7 +25,7 @@ describe('feature/relations/types/has_many_retrieve', () => { @Str('') title!: string } - it('can eager load has one relation', async () => { + it('can eager load has many relation', async () => { const store = createStore([User, Post]) fillState(store, { diff --git a/test/feature/repository/retrieves_find.spec.ts b/test/feature/repository/retrieves_find.spec.ts index 5249ce42..4a9c35ab 100644 --- a/test/feature/repository/retrieves_find.spec.ts +++ b/test/feature/repository/retrieves_find.spec.ts @@ -2,6 +2,7 @@ import { createStore, fillState, assertInstanceOf, + assertModel, assertModels } from 'test/Helpers' import { Model, Attr, Str } from '@/index' @@ -25,12 +26,10 @@ describe('feature/repository/retrieves_find', () => { } }) - const user = store.$repo(User).find(2) - - const expected = { id: 2, name: 'Jane Doe' } + const user = store.$repo(User).find(2)! expect(user).toBeInstanceOf(User) - expect(user).toEqual(expected) + assertModel(user, { id: 2, name: 'Jane Doe' }) }) it('returns `null` if the reocrd is not found', () => { @@ -62,13 +61,11 @@ describe('feature/repository/retrieves_find', () => { const users = store.$repo(User).find([1, 3]) - const expected = [ - { id: 1, name: 'John Doe' }, - { id: 3, name: 'Johnny Doe' } - ] - expect(users.length).toBe(2) assertInstanceOf(users, User) - assertModels(users, expected) + assertModels(users, [ + { id: 1, name: 'John Doe' }, + { id: 3, name: 'Johnny Doe' } + ]) }) })