diff --git a/package.json b/package.json index 70c5278..e630ad2 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "vuex": ">=3.1.0" }, "dependencies": { - "normalizr": "^3.6.0" + "@types/uuid": "^7.0.3", + "normalizr": "^3.6.0", + "uuid": "^8.0.0" }, "devDependencies": { "@microsoft/api-documenter": "^7.8.0", diff --git a/rollup.config.js b/rollup.config.js index 39ffed4..569b4d3 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -46,7 +46,7 @@ function createEntry(config) { : config.env !== 'production' })) - c.plugins.push(resolve()) + c.plugins.push(resolve({ browser: true })) c.plugins.push(commonjs()) c.plugins.push(ts({ diff --git a/src/index.cjs.ts b/src/index.cjs.ts index bf05c08..f39993c 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -10,6 +10,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 { Uid } from './model/decorators/attributes/types/Uid' import { HasOne } from './model/decorators/attributes/relations/HasOne' import { BelongsTo } from './model/decorators/attributes/relations/BelongsTo' import { HasMany } from './model/decorators/attributes/relations/HasMany' @@ -19,6 +20,7 @@ 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 { Uid as UidAttr } from './model/attributes/types/Uid' 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' @@ -38,6 +40,7 @@ export default { Str, Num, Bool, + Uid, HasOne, BelongsTo, HasMany, @@ -47,6 +50,7 @@ export default { StringAttr, NumberAttr, BooleanAttr, + UidAttr, Relation, HasOneAttr, HasManyAttr, diff --git a/src/index.ts b/src/index.ts index 74a10c3..b680879 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ 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/types/Uid' export * from './model/decorators/attributes/relations/HasOne' export * from './model/decorators/attributes/relations/BelongsTo' export * from './model/decorators/attributes/relations/HasMany' @@ -20,6 +21,7 @@ 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 { Uid as UidAttr } from './model/attributes/types/Uid' export * from './model/attributes/relations/Relation' export { HasOne as HasOneAttr } from './model/attributes/relations/HasOne' export { BelongsTo as BelongsToAttr } from './model/attributes/relations/BelongsTo' @@ -49,6 +51,7 @@ 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 { Uid as UidAttr } from './model/attributes/types/Uid' 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' @@ -70,6 +73,7 @@ export default { StringAttr, NumberAttr, BooleanAttr, + UidAttr, Relation, HasOneAttr, HasManyAttr, diff --git a/src/model/Model.ts b/src/model/Model.ts index f400b8a..ad7774f 100644 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -1,11 +1,13 @@ import { Store } from 'vuex' -import { isArray, assert } from '../support/Utils' +import { isNullish, isArray, assert } from '../support/Utils' import { Element, Item, Collection } from '../data/Data' +import { Database } from '../database/Database' 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 { Uid } from './attributes/types/Uid' import { Relation } from './attributes/relations/Relation' import { HasOne } from './attributes/relations/HasOne' import { BelongsTo } from './attributes/relations/BelongsTo' @@ -17,6 +19,7 @@ export type ModelRegistries = Record export type ModelRegistry = Record Attribute> export interface ModelOptions { + fill?: boolean relations?: boolean } @@ -57,10 +60,12 @@ export class Model { /** * Create a new model instance. */ - constructor(attributes?: Element, options?: ModelOptions) { + constructor(attributes?: Element, options: ModelOptions = {}) { this.$boot() - this.$fill(attributes, options) + const fill = options.fill ?? true + + fill && this.$fill(attributes, options) // Prevent `_store` from becoming cyclic object value and causing // v-bind side-effects by negating enumerability. @@ -114,32 +119,61 @@ export class Model { this.schemas = {} } + /** + * Clear registries. + */ + static clearRegistries(): void { + this.registries = {} + } + + /** + * Create a new model instance without field values being populated. + * + * This method is mainly fo the internal use when registering models to the + * database. Since all pre-registered models are for referencing its model + * setting during the various process, but the fields are not required. + * + * Use this method when you want create a new model instance for: + * - Registering model to a component (eg. Repository, Query, etc.) + * - Registering model to attributes (String, Has Many, etc.) + */ + static newRawInstance(this: M): InstanceType { + return new this(undefined, { fill: false }) as InstanceType + } + /** * Create a new Attr attribute instance. */ static attr(value: any): Attr { - return new Attr(new this(), value) + return new Attr(this.newRawInstance(), value) } /** * Create a new String attribute instance. */ static string(value: string | null): Str { - return new Str(new this(), value) + return new Str(this.newRawInstance(), value) } /** * Create a new Number attribute instance. */ static number(value: number | null): Num { - return new Num(new this(), value) + return new Num(this.newRawInstance(), value) } /** * Create a new Boolean attribute instance. */ static boolean(value: boolean | null): Bool { - return new Bool(new this(), value) + return new Bool(this.newRawInstance(), value) + } + + /** + * Create a new Uid attribute instance. + */ + static uid(): Uid { + return new Uid(this.newRawInstance()) } /** @@ -150,11 +184,11 @@ export class Model { foreignKey: string, localKey?: string ): HasOne { - const model = new this() + const model = this.newRawInstance() localKey = localKey ?? model.$getLocalKey() - return new HasOne(model, new related(), foreignKey, localKey) + return new HasOne(model, related.newRawInstance(), foreignKey, localKey) } /** @@ -165,11 +199,11 @@ export class Model { foreignKey: string, ownerKey?: string ): BelongsTo { - const instance = new related() + const instance = related.newRawInstance() ownerKey = ownerKey ?? instance.$getLocalKey() - return new BelongsTo(new this(), instance, foreignKey, ownerKey) + return new BelongsTo(this.newRawInstance(), instance, foreignKey, ownerKey) } /** @@ -180,11 +214,11 @@ export class Model { foreignKey: string, localKey?: string ): HasMany { - const model = new this() + const model = this.newRawInstance() localKey = localKey ?? model.$getLocalKey() - return new HasMany(model, new related(), foreignKey, localKey) + return new HasMany(model, related.newRawInstance(), foreignKey, localKey) } /** @@ -207,6 +241,13 @@ export class Model { return this._store } + /** + * Get the database instance. + */ + get $database(): Database { + return this.$store.$database + } + /** * Get the entity for this model. */ @@ -316,10 +357,11 @@ export class Model { /** * Get the index id of this model or for a given record. */ - $getIndexId(record?: Element): string { + $getIndexId(record?: Element): string | null { const target = record ?? this + const id = target[this.$primaryKey] - return String(target[this.$primaryKey]) + return isNullish(id) ? null : String(id) } /** diff --git a/src/model/ModelConstructor.ts b/src/model/ModelConstructor.ts new file mode 100644 index 0000000..9599901 --- /dev/null +++ b/src/model/ModelConstructor.ts @@ -0,0 +1,6 @@ +import { Model } from './Model' + +export interface ModelConstructor { + new (...args: any[]): M + newRawInstance(): M +} diff --git a/src/model/attributes/types/Uid.ts b/src/model/attributes/types/Uid.ts new file mode 100644 index 0000000..7e393fe --- /dev/null +++ b/src/model/attributes/types/Uid.ts @@ -0,0 +1,11 @@ +import { v1 as uuid } from 'uuid' +import { Type } from './Type' + +export class Uid extends Type { + /** + * Make the value for the attribute. + */ + make(value: any): string { + return value ?? uuid() + } +} diff --git a/src/model/decorators/attributes/types/Uid.ts b/src/model/decorators/attributes/types/Uid.ts new file mode 100644 index 0000000..86a54d7 --- /dev/null +++ b/src/model/decorators/attributes/types/Uid.ts @@ -0,0 +1,10 @@ +import { PropertyDecorator } from '../../Contracts' + +/** + * Create a Uid attribute property decorator. + */ +export function Uid(): PropertyDecorator { + return (target, propertyKey) => { + target.$self.setRegistry(propertyKey, () => target.$self.uid()) + } +} diff --git a/src/plugin/Plugin.ts b/src/plugin/Plugin.ts index 64e9976..488e6f5 100644 --- a/src/plugin/Plugin.ts +++ b/src/plugin/Plugin.ts @@ -7,6 +7,7 @@ import { Attr } from '../model/attributes/types/Attr' import { String } from '../model/attributes/types/String' import { Number } from '../model/attributes/types/Number' import { Boolean } from '../model/attributes/types/Boolean' +import { Uid } from '../model/attributes/types/Uid' import { Relation } from '../model/attributes/relations/Relation' import { HasOne } from '../model/attributes/relations/HasOne' import { HasMany } from '../model/attributes/relations/HasMany' @@ -38,6 +39,7 @@ export interface VuexORMPluginComponents { String: typeof String Number: typeof Number Boolean: typeof Boolean + Uid: typeof Uid Relation: typeof Relation HasOne: typeof HasOne HasMany: typeof HasMany @@ -67,6 +69,7 @@ export const components: VuexORMPluginComponents = { String, Number, Boolean, + Uid, Relation, HasOne, HasMany, diff --git a/src/query/Query.ts b/src/query/Query.ts index ee99ad2..dbf84ff 100644 --- a/src/query/Query.ts +++ b/src/query/Query.ts @@ -455,7 +455,7 @@ export class Query { const recordsArray = isArray(records) ? records : [records] return recordsArray.reduce>((collection, record) => { - const model = this.pick(this.model.$getIndexId(record)) + const model = this.pick(this.model.$getIndexId(record)!) model && collection.push(model.$fill(record)) @@ -615,7 +615,7 @@ export class Query { * Get an array of ids from the given collection. */ protected getIndexIdsFromCollection(models: Collection): string[] { - return models.map((model) => model.$getIndexId()) + return models.map((model) => model.$getIndexId()!) } /** @@ -644,7 +644,7 @@ export class Query { const modelArray = isArray(models) ? models : [models] return modelArray.reduce((records, model) => { - records[model.$getIndexId()] = model.$getAttributes() + records[model.$getIndexId()!] = model.$getAttributes() return records }, {}) } diff --git a/src/repository/Repository.ts b/src/repository/Repository.ts index 172156f..04bd827 100644 --- a/src/repository/Repository.ts +++ b/src/repository/Repository.ts @@ -1,8 +1,9 @@ import { Store } from 'vuex' -import { assert } from '../support/Utils' import { Constructor } from '../types' +import { assert } from '../support/Utils' import { Element, Item, Collection, Collections } from '../data/Data' import { Model } from '../model/Model' +import { ModelConstructor } from '../model/ModelConstructor' import { Query } from '../query/Query' import { WherePrimaryClosure, @@ -44,10 +45,10 @@ export class Repository { /** * Initialize the repository by setting the model instance. */ - initialize(model?: Constructor): this { + initialize(model?: ModelConstructor): this { // If there's a model passed in, just use that and return immediately. if (model) { - this.model = new model().$setStore(this.store) + this.model = model.newRawInstance().$setStore(this.store) return this } @@ -57,7 +58,7 @@ export class Repository { // In this case, we'll check if the user has set model to the `use` // property and instantiate that. if (this.use) { - this.model = (new this.use() as M).$setStore(this.store) + this.model = (this.use.newRawInstance() as M).$setStore(this.store) return this } diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index 3b5487d..ada9b7c 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -1,4 +1,6 @@ import { schema as Normalizr, Schema as NormalizrSchema } from 'normalizr' +import { isNullish, assert } from '../support/Utils' +import { Uid } from '../model/attributes/types/Uid' import { Relation } from '../model/attributes/relations/Relation' import { Model } from '../model/Model' @@ -59,12 +61,37 @@ export class Schema { } /** - * The id attribute option for the normalizr entity. + * The `id` attribute option for the normalizr entity. + * + * Generates any missing primary keys declared by a Uid attribute. Missing + * primary keys where the designated attributes do not exist will + * throw an error. + * + * Note that this will only generate uids for primary key attributes since it + * is required to generate the "index id" while the other attributes are not. + * + * It's especially important when attempting to "update" records since we'll + * want to retain the missing attributes in-place to prevent them being + * overridden by newly generated uid values. + * + * If uid primary keys are omitted, when invoking the "update" method, it will + * fail because the uid values will never exist in the store. + * + * While it would be nice to throw an error in such a case, instead of + * silently failing an update, we don't have a way to detect whether users + * are trying to "update" records or "inserting" new records at this stage. + * Something to consider for future revisions. */ private idAttribute( model: Model, parent: Model ): Normalizr.StrategyFunction { + // We'll first check if the model contains any uid attributes. If so, we + // generate the uids during the normalization process, so we'll keep that + // check result here. This way, we can use this result while processing each + // record, instead of looping through the model fields each time. + const uidFields = this.getUidPrimaryKeyPairs(model) + return (record, parentRecord, key) => { // If the `key` is not `null`, that means this record is a nested // relationship of the parent model. In this case, we'll attach any @@ -73,10 +100,47 @@ export class Schema { ;(parent.$fields[key] as Relation).attach(parentRecord, record) } - return model.$getIndexId(record) + // Next, we'll generate any missing primary key fields defined as + // uid field. + for (const key in uidFields) { + if (isNullish(record[key])) { + record[key] = uidFields[key].make(record[key]) + } + } + + // Finally, we'll check if the model has a valid index id. If not, that + // means users have passed in the record without a primary key, and the + // primary key field is not defined as uid field. In this case, we'll + // throw an error. Otherwise, everything is fine, so let's return the + // index id. + const indexId = model.$getIndexId(record) + + assert(!isNullish(indexId), [ + 'The record is missing the primary key. If you want to persist record', + 'without the primary key, please defined the primary key field as', + '`uid` field.' + ]) + + return indexId } } + /** + * Get all primary keys defined by the Uid attribute for the given model. + */ + private getUidPrimaryKeyPairs(model: Model): Record { + const attributes = {} as Record + + const key = model.$getKeyName() + const attr = model.$fields[key] + + if (attr instanceof Uid) { + attributes[key] = attr + } + + return attributes + } + /** * Create a definition for the given model. */ diff --git a/test/Helpers.ts b/test/Helpers.ts index 60c45b0..74b1d15 100644 --- a/test/Helpers.ts +++ b/test/Helpers.ts @@ -1,3 +1,4 @@ +import { v1 as uuid } from 'uuid' import Vue from 'vue' import Vuex, { Store } from 'vuex' import VuexORM, { @@ -68,3 +69,7 @@ export function assertInstanceOf( expect(item).toBeInstanceOf(model) }) } + +export function mockUid(ids: any[]): void { + ids.forEach((id) => (uuid as jest.Mock).mockImplementationOnce(() => id)) +} diff --git a/test/feature/relations/types/belongs_to_insert_uid.spec.ts b/test/feature/relations/types/belongs_to_insert_uid.spec.ts new file mode 100644 index 0000000..f682ab2 --- /dev/null +++ b/test/feature/relations/types/belongs_to_insert_uid.spec.ts @@ -0,0 +1,84 @@ +import { createStore, assertState, mockUid } from 'test/Helpers' +import { Model, Attr, Uid, Str, BelongsTo } from '@/index' + +describe('feature/relations/types/belongs_to_insert_uid', () => { + beforeEach(() => { + Model.clearRegistries() + }) + + it('inserts "belongs to" relation with parent having "uid" field as the primary key', async () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: number + @Str('') name!: string + } + + class Post extends Model { + static entity = 'posts' + + @Uid() id!: string + @Attr() userId!: number | null + @Str('') title!: string + + @BelongsTo(() => User, 'userId') + author!: User | null + } + + mockUid(['uid1']) + + const store = createStore() + + await store.$repo(Post).insert({ + title: 'Title 01', + author: { id: 1, name: 'John Doe' } + }) + + assertState(store, { + users: { + 1: { id: 1, name: 'John Doe' } + }, + posts: { + uid1: { id: 'uid1', userId: 1, title: 'Title 01' } + } + }) + }) + + it('inserts "belongs to" relation with child having "uid" as the owner key', async () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string + @Str('') name!: string + } + + class Post extends Model { + static entity = 'posts' + + @Uid() id!: number + @Attr() userId!: number | null + @Str('') title!: string + + @BelongsTo(() => User, 'userId') + author!: User | null + } + + mockUid(['uid1', 'uid2']) + + const store = createStore() + + await store.$repo(Post).insert({ + title: 'Title 01', + author: { name: 'John Doe' } + }) + + assertState(store, { + users: { + uid2: { id: 'uid2', name: 'John Doe' } + }, + posts: { + uid1: { id: 'uid1', userId: 'uid2', title: 'Title 01' } + } + }) + }) +}) diff --git a/test/feature/relations/types/has_many_insert_uid.spec.ts b/test/feature/relations/types/has_many_insert_uid.spec.ts new file mode 100644 index 0000000..f5ebb54 --- /dev/null +++ b/test/feature/relations/types/has_many_insert_uid.spec.ts @@ -0,0 +1,92 @@ +import { createStore, assertState, mockUid } from 'test/Helpers' +import { Model, Attr, Uid, Str, HasMany } from '@/index' + +describe('feature/relations/types/has_many_insert_uid', () => { + beforeEach(() => { + Model.clearRegistries() + }) + + it('inserts "has many" relation with parent having "uid" field as the primary key', async () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string + @Str('') name!: string + + @HasMany(() => Post, 'userId') + posts!: Post[] + } + + class Post extends Model { + static entity = 'posts' + + @Attr() id!: number + @Attr() userId!: number + @Str('') title!: string + } + + mockUid(['uid1']) + + const store = createStore() + + await store.$repo(User).insert({ + name: 'John Doe', + posts: [ + { id: 1, title: 'Title 01' }, + { id: 2, title: 'Title 02' } + ] + }) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 'uid1', title: 'Title 01' }, + 2: { id: 2, userId: 'uid1', title: 'Title 02' } + } + }) + }) + + it('inserts "has many" relation with child having "uid" as the foreign key', async () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string + @Str('') name!: string + + @HasMany(() => Post, 'userId') + posts!: Post[] + } + + class Post extends Model { + static entity = 'posts' + + @Attr() id!: number + @Uid() userId!: string + @Str('') title!: string + } + + mockUid(['uid1', 'uid2', 'uid3']) + + const store = createStore() + + await store.$repo(User).insert({ + name: 'John Doe', + posts: [ + { id: 1, title: 'Title 01' }, + { id: 2, title: 'Title 02' } + ] + }) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' } + }, + posts: { + 1: { id: 1, userId: 'uid1', title: 'Title 01' }, + 2: { id: 2, userId: 'uid1', title: 'Title 02' } + } + }) + }) +}) diff --git a/test/feature/relations/types/has_one_insert_uid.spec.ts b/test/feature/relations/types/has_one_insert_uid.spec.ts new file mode 100644 index 0000000..2328db7 --- /dev/null +++ b/test/feature/relations/types/has_one_insert_uid.spec.ts @@ -0,0 +1,130 @@ +import { createStore, assertState, mockUid } from 'test/Helpers' +import { Model, Attr, Uid, Str, HasOne } from '@/index' + +describe('feature/relations/types/has_one_insert_uid', () => { + beforeEach(() => { + Model.clearRegistries() + }) + + it('inserts "has one" relation with parent having "uid" field as the primary key', async () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string + @Str('') name!: string + + @HasOne(() => Phone, 'userId') + phone!: Phone | null + } + + class Phone extends Model { + static entity = 'phones' + + @Attr() id!: number + @Attr() userId!: string + @Str('') number!: string + } + + mockUid(['uid1']) + + const store = createStore() + + await store.$repo(User).insert({ + name: 'John Doe', + phone: { + id: 1, + number: '123-4567-8912' + } + }) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' } + }, + phones: { + 1: { id: 1, userId: 'uid1', number: '123-4567-8912' } + } + }) + }) + + it('inserts "has one" relation with child having "uid" as the foreign key', async () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string + @Str('') name!: string + + @HasOne(() => Phone, 'userId') + phone!: Phone | null + } + + class Phone extends Model { + static entity = 'phones' + + @Uid() id!: string + @Uid() userId!: string + @Str('') number!: string + } + + mockUid(['uid1', 'uid2']) + + const store = createStore() + + await store.$repo(User).insert({ + name: 'John Doe', + phone: { + number: '123-4567-8912' + } + }) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' } + }, + phones: { + uid2: { id: 'uid2', userId: 'uid1', number: '123-4567-8912' } + } + }) + }) + + it('inserts "has one" relation with child having "uid" as foreign key being primary key', async () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string + @Str('') name!: string + + @HasOne(() => Phone, 'userId') + phone!: Phone | null + } + + class Phone extends Model { + static entity = 'phones' + + static primaryKey = 'userId' + + @Uid() userId!: string + @Str('') number!: string + } + + mockUid(['uid1', 'uid2']) + + const store = createStore() + + await store.$repo(User).insert({ + name: 'John Doe', + phone: { + number: '123-4567-8912' + } + }) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' } + }, + phones: { + uid1: { userId: 'uid1', number: '123-4567-8912' } + } + }) + }) +}) diff --git a/test/feature/repository/inserts_fresh_uid.spec.ts b/test/feature/repository/inserts_fresh_uid.spec.ts new file mode 100644 index 0000000..32a768f --- /dev/null +++ b/test/feature/repository/inserts_fresh_uid.spec.ts @@ -0,0 +1,26 @@ +import { createStore, assertState, mockUid } from 'test/Helpers' +import { Model, Uid, Str } from '@/index' + +describe('feature/uid/inserts_fresh_uid', () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string | null + @Str('') name!: string + } + + it('generates unique ids if the model field contains a `uid` attribute', async () => { + mockUid(['uid1', 'uid2']) + + const store = createStore() + + await store.$repo(User).fresh([{ name: 'John Doe' }, { name: 'Jane Doe' }]) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' }, + uid2: { id: 'uid2', name: 'Jane Doe' } + } + }) + }) +}) diff --git a/test/feature/repository/inserts_insert_uid.spec.ts b/test/feature/repository/inserts_insert_uid.spec.ts new file mode 100644 index 0000000..269b0c8 --- /dev/null +++ b/test/feature/repository/inserts_insert_uid.spec.ts @@ -0,0 +1,26 @@ +import { createStore, assertState, mockUid } from 'test/Helpers' +import { Model, Uid, Str } from '@/index' + +describe('feature/uid/inserts_insert_uid', () => { + class User extends Model { + static entity = 'users' + + @Uid() id!: string | null + @Str('') name!: string + } + + it('generates unique ids if the model field contains a `uid` attribute', async () => { + mockUid(['uid1', 'uid2']) + + const store = createStore() + + await store.$repo(User).insert([{ name: 'John Doe' }, { name: 'Jane Doe' }]) + + assertState(store, { + users: { + uid1: { id: 'uid1', name: 'John Doe' }, + uid2: { id: 'uid2', name: 'Jane Doe' } + } + }) + }) +}) diff --git a/test/setup.ts b/test/setup.ts index 6c5aa66..628a0f5 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,5 +1,9 @@ import { Model } from '@/index' +jest.mock('uuid', () => ({ + v1: jest.fn() +})) + beforeEach(() => { Model.clearBootedModels() }) diff --git a/test/unit/model/Model_Attrs_UID.spec.ts b/test/unit/model/Model_Attrs_UID.spec.ts new file mode 100644 index 0000000..dc50f04 --- /dev/null +++ b/test/unit/model/Model_Attrs_UID.spec.ts @@ -0,0 +1,18 @@ +import { mockUid } from 'test/Helpers' +import { Uid } from '@/model/decorators/attributes/types/Uid' +import { Model } from '@/model/Model' + +describe('unit/model/Model_Attrs_UID', () => { + it('returns `null` when the model is instantiated', () => { + class User extends Model { + static entity = 'users' + + @Uid() + id!: string + } + + mockUid(['uid1']) + + expect(new User().id).toBe('uid1') + }) +}) diff --git a/yarn.lock b/yarn.lock index 5371752..fdd4fad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1415,6 +1415,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/uuid@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.3.tgz#45cd03e98e758f8581c79c535afbd4fc27ba7ac8" + integrity sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -10396,6 +10401,11 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + v8-to-istanbul@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.3.tgz#22fe35709a64955f49a08a7c7c959f6520ad6f20"