diff --git a/__test__/global-plugin.spec.ts b/__test__/global-plugin.spec.ts index 4786d37a..e436f3e8 100644 --- a/__test__/global-plugin.spec.ts +++ b/__test__/global-plugin.spec.ts @@ -81,7 +81,6 @@ describe('Global Plugin', () => { await startInTest(getDefaultInstance()); const doc = await GlobalPlugins.create(schemaData); - console.log(doc); expect(doc.operational).toBe(false); // plugin expect(doc.plugin).toBe('registered from plugin 2!'); // plugin2 }); diff --git a/__test__/nested-model-key.spec.ts b/__test__/nested-model-key.spec.ts new file mode 100644 index 00000000..5fd1071b --- /dev/null +++ b/__test__/nested-model-key.spec.ts @@ -0,0 +1,93 @@ +import { getDefaultInstance, model } from '../src'; +import { startInTest } from './testData'; + +const accessDoc = { + type: 'airlineR', + isActive: false, + name: 'Ottoman Access', +}; + +const accessDoc2 = { + type: 'airlineNested', + isActive: false, + name: 'Ottoman Access Nested', +}; + +const updateDoc = { + isActive: true, +}; + +const replaceDoc = { + type: 'airlineNested Replace', + isActive: false, +}; + +const schema = { + type: String, + isActive: Boolean, + name: String, +}; + +interface IUser { + type: string; + name?: string; + isActive?: boolean; +} + +describe('nested model key', function () { + test('UserModel.create Creating a document', async () => { + const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); + await startInTest(getDefaultInstance()); + const result = await UserModel.create(accessDoc); + expect(result.id).toBeDefined(); + }); + + test('UserModel.findById Get a document', async () => { + const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); + await startInTest(getDefaultInstance()); + const result = await UserModel.create(accessDoc); + const user = await UserModel.findById(result.id); + expect(user.name).toBeDefined(); + }); + + test('UserModel.update -> Update a document', async () => { + const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); + await startInTest(getDefaultInstance()); + const result = await UserModel.create(accessDoc); + await UserModel.updateById(result.id, updateDoc); + const user = await UserModel.findById(result.id); + expect(user.isActive).toBe(true); + }); + + test('UserModel.replace Replace a document', async () => { + const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); + await startInTest(getDefaultInstance()); + const result = await UserModel.create(accessDoc); + await UserModel.replaceById(result.id, replaceDoc); + const user = await UserModel.findById(result.id); + expect(user.type).toBe('airlineNested Replace'); + expect(user.name).toBeUndefined(); + }); + + test('Document.save Save and update a document', async () => { + const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); + await startInTest(getDefaultInstance()); + const user = new UserModel(accessDoc2); + const result = await user.save(); + expect(user.id).toBeDefined(); + user.name = 'Instance Edited'; + user.id = result.id; + const updated = await user.save(); + expect(updated.name).toBe('Instance Edited'); + }); + + test('Remove saved document from Model instance', async () => { + const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); + await startInTest(getDefaultInstance()); + const user = new UserModel(accessDoc2); + await user.save(); + const removed = await user.remove(); + expect(user.id).toBeDefined(); + expect(removed.cas).toBeDefined(); + }); +}); diff --git a/__test__/utils.spec.ts b/__test__/utils.spec.ts index 52b8dedf..d412520f 100644 --- a/__test__/utils.spec.ts +++ b/__test__/utils.spec.ts @@ -1,5 +1,5 @@ import { bucketName, connectionString, connectUri, password, username } from './testData'; -import { model, Ottoman, set, ValidationError } from '../src'; +import { model, Ottoman, set, ValidationError, setValueByPath, getValueByPath } from '../src'; import { isModel } from '../src/utils/is-model'; import { extractConnectionString } from '../src/utils/extract-connection-string'; import { is } from '../src'; @@ -213,3 +213,43 @@ test('set function fail', async () => { expect(e.message).toBe('set second argument must be number | string | boolean value'); } }); + +test('set value', () => { + const doc: any = { name: 'Robert' }; + setValueByPath(doc, 'meta.id', 'id'); + expect(doc.name).toBe('Robert'); + expect(doc.meta.id).toBe('id'); +}); + +test('set value 5 level deep', () => { + const doc: any = { name: 'Robert' }; + setValueByPath(doc, 'l1.l2.l3.l4.l5', 'id'); + expect(doc.name).toBe('Robert'); + expect(doc.l1.l2.l3.l4.l5).toBe('id'); +}); + +test('set value keep nested object values', () => { + const doc: any = { name: 'Robert', meta: { page: 1 } }; + setValueByPath(doc, 'meta.id', 'id'); + expect(doc.name).toBe('Robert'); + expect(doc.meta.id).toBe('id'); + expect(doc.meta.page).toBe(1); +}); + +test('get value on nested object by path', () => { + const doc: any = { name: 'Robert', meta: { page: 1 } }; + const valueInPath = getValueByPath(doc, 'meta.page'); + expect(valueInPath).toBe(1); +}); + +test('get value on nested object by non-exists path', () => { + const doc: any = { name: 'Robert', meta: { page: 1 } }; + const valueInPath = getValueByPath(doc, 'meta.page.x.y'); + expect(valueInPath).toBe(undefined); +}); + +test('get value on nested object by non-exists path', () => { + const doc: any = { name: 'Robert', meta: { page: 1, l1: { l2: 'nested value' } } }; + const valueInPath = getValueByPath(doc, 'meta.l1.l2'); + expect(valueInPath).toBe('nested value'); +}); diff --git a/src/handler/find/find.ts b/src/handler/find/find.ts index 2642cef2..25be0ec8 100644 --- a/src/handler/find/find.ts +++ b/src/handler/find/find.ts @@ -1,4 +1,4 @@ -import { getModelMetadata, SearchConsistency } from '../..'; +import { getModelMetadata, SearchConsistency, setValueByPath } from '../..'; import { ModelMetadata } from '../../model/interfaces/model-metadata.interface'; import { getPopulated, PopulateAuxOptionsType } from '../../model/utils/model.utils'; import { LogicalWhereExpr, Query } from '../../query'; diff --git a/src/model/create-model.ts b/src/model/create-model.ts index 4f94f32a..263dc12c 100644 --- a/src/model/create-model.ts +++ b/src/model/create-model.ts @@ -1,5 +1,5 @@ import { DocumentNotFoundError, DropCollectionOptions } from 'couchbase'; -import { Query, SearchConsistency } from '..'; +import { getValueByPath, Query, SearchConsistency, setValueByPath } from '..'; import { BuildIndexQueryError, OttomanError } from '../exceptions/ottoman-errors'; import { createMany, find, FindOptions, ManyQueryResponse, removeMany, updateMany } from '../handler'; import { FindByIdOptions, IFindOptions } from '../handler/'; @@ -230,7 +230,9 @@ export const _buildModel = (metadata: ModelMetadata) => { const value = await _Model.findById(key, { withExpiry: !!options.maxExpiry }); if (value[ID_KEY]) { const strategy = CAST_STRATEGY.THROW; - (value as Model)._applyData({ ...value, ...data, ...{ [modelKey]: value[modelKey] } }, options.strict); + const modelKeyObj = {}; + setValueByPath(modelKeyObj, modelKey, getValueByPath(value, modelKey)); + (value as Model)._applyData({ ...value, ...data, ...modelKeyObj }, options.strict); const instance = new _Model({ ...value }, { strategy }); const _options: any = {}; if (options.maxExpiry) { @@ -262,10 +264,13 @@ export const _buildModel = (metadata: ModelMetadata) => { } const replace = new _Model({ ...temp }).$wasNew(); + const modelKeyObj = {}; + setValueByPath(modelKeyObj, modelKey, modelName); replace._applyData( { ...data, - ...{ [ID_KEY]: key, [modelKey]: modelName }, + ...{ [ID_KEY]: key }, + ...modelKeyObj, }, options?.strict, ); @@ -281,7 +286,9 @@ export const _buildModel = (metadata: ModelMetadata) => { }; static removeById = (id: string) => { - const instance = new _Model({ ...{ [ID_KEY]: id, [modelKey]: modelName } }); + const modelKeyObj = {}; + setValueByPath(modelKeyObj, modelKey, modelName); + const instance = new _Model({ ...{ [ID_KEY]: id, ...modelKeyObj } }); return instance.remove(); }; diff --git a/src/model/document.ts b/src/model/document.ts index 0bdb22fc..909bf251 100644 --- a/src/model/document.ts +++ b/src/model/document.ts @@ -14,6 +14,7 @@ import { getModelRefKeys } from './utils/get-model-ref-keys'; import { getModelMetadata, getPopulated } from './utils/model.utils'; import { removeLifeCycle } from './utils/remove-life-cycle'; import { storeLifeCycle } from './utils/store-life-cycle'; +import { setValueByPath } from '../utils'; export type IDocument = Document & T; @@ -182,7 +183,9 @@ export abstract class Document { } } } - const addedMetadata = { ...data, [modelKey]: modelName }; + const modelKeyObj = {}; + setValueByPath(modelKeyObj, modelKey, modelName); + const addedMetadata = { ...data, ...modelKeyObj }; const { document } = await storeLifeCycle({ key, id, data: addedMetadata, options: _options, metadata, refKeys }); return this._applyData(document).$wasNew(); } diff --git a/src/model/index/n1ql/ensure-n1ql-indexes.ts b/src/model/index/n1ql/ensure-n1ql-indexes.ts index f6fe2944..3e95c2d5 100644 --- a/src/model/index/n1ql/ensure-n1ql-indexes.ts +++ b/src/model/index/n1ql/ensure-n1ql-indexes.ts @@ -23,8 +23,11 @@ export const ensureN1qlIndexes = async (ottoman: Ottoman, n1qlIndexes) => { const Model = ottoman.getModel(key); const metadata = getModelMetadata(Model); const { modelName, modelKey, scopeName, collectionName } = metadata; + const scapedModelKey = modelKey.replace(/./g, 'dot'); const name = - collectionName !== DEFAULT_COLLECTION ? `Ottoman${scopeName}${modelName}` : `Ottoman${scopeName}${modelKey}`; + collectionName !== DEFAULT_COLLECTION + ? `Ottoman${scopeName}${modelName}` + : `Ottoman${scopeName}${scapedModelKey}`; if (!existingIndexesNames.includes(name)) { const on = collectionName !== DEFAULT_COLLECTION diff --git a/src/utils/getValueByPath.ts b/src/utils/getValueByPath.ts new file mode 100644 index 00000000..f9552eda --- /dev/null +++ b/src/utils/getValueByPath.ts @@ -0,0 +1,2 @@ +export const getValueByPath = >(obj: T, path: string, separator = '.') => + path.split(separator).reduce((prev, curr) => prev && prev[curr], obj); diff --git a/src/utils/index.ts b/src/utils/index.ts index ca273abd..03d4802c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,5 @@ export { isDocumentNotFoundError } from './is-not-found'; export { VALIDATION_STRATEGY } from './validation.strategy'; export { is, isSchemaTypeSupported, isSchemaFactoryType } from './is-type'; export { set } from './environment-set'; +export { setValueByPath } from './setValueByPath'; +export { getValueByPath } from './getValueByPath'; diff --git a/src/utils/setValueByPath.ts b/src/utils/setValueByPath.ts new file mode 100644 index 00000000..7e517e1b --- /dev/null +++ b/src/utils/setValueByPath.ts @@ -0,0 +1,12 @@ +export const setValueByPath = >(object: T, path: string, value: any) => { + path = path.replace(/[\[]/gm, '.').replace(/[\]]/gm, ''); //to accept [index] + const keys = path.split('.'); + const last = keys.pop(); + if (!last) { + return; + } + + keys.reduce(function (o, k) { + return (o[k] = o[k] || {}); + }, object)[last] = value; +};