Skip to content

Commit

Permalink
feat(ottoman): add support for nested modelKey
Browse files Browse the repository at this point in the history
Closes: "#638"
  • Loading branch information
gsi-alejandro committed Mar 14, 2022
1 parent 208f88a commit 5f9898e
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 9 deletions.
1 change: 0 additions & 1 deletion __test__/global-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
93 changes: 93 additions & 0 deletions __test__/nested-model-key.spec.ts
Original file line number Diff line number Diff line change
@@ -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<IUser>('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<IUser>('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<IUser>('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();
});
});
42 changes: 41 additions & 1 deletion __test__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
});
2 changes: 1 addition & 1 deletion src/handler/find/find.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
15 changes: 11 additions & 4 deletions src/model/create-model.ts
Original file line number Diff line number Diff line change
@@ -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/';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
);
Expand All @@ -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();
};

Expand Down
5 changes: 4 additions & 1 deletion src/model/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = any> = Document & T;

Expand Down Expand Up @@ -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();
}
Expand Down
5 changes: 4 additions & 1 deletion src/model/index/n1ql/ensure-n1ql-indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/utils/getValueByPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const getValueByPath = <T = Record<string, any>>(obj: T, path: string, separator = '.') =>
path.split(separator).reduce((prev, curr) => prev && prev[curr], obj);
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
12 changes: 12 additions & 0 deletions src/utils/setValueByPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const setValueByPath = <T = Record<string, any>>(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;
};

0 comments on commit 5f9898e

Please sign in to comment.