From c2abb5672936e3227c38151ac360d22533b534d3 Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Fri, 23 May 2025 10:30:47 -0400 Subject: [PATCH] feat(properties): add ForeignKeyProperty --- .nycrc | 23 +++++ README.md | 12 +++ package-lock.json | 168 +++++++++++++++++++++++++++++++- package.json | 35 +------ src/orm/properties.ts | 75 +++++++++++++- src/properties.ts | 33 +++++++ test/src/index.test.ts | 9 ++ test/src/orm/properties.test.ts | 70 +++++++++++++ test/src/orm/query.test.ts | 78 +++++++++++++++ test/src/utils.test.ts | 2 +- 10 files changed, 466 insertions(+), 39 deletions(-) create mode 100644 .nycrc create mode 100644 test/src/index.test.ts diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..baa468b --- /dev/null +++ b/.nycrc @@ -0,0 +1,23 @@ +{ + "branches": 90, + "lines": 98, + "functions": 98, + "statements": 98, + "extends": "@istanbuljs/nyc-config-typescript", + "check-coverage": true, + "all": true, + "include": ["src/**/!(*.test.*).[tj]s?(x)"], + "exclude": [ + "src/index.ts", + "src/**/index.ts", + "src/_tests_/**/*.*", + "node_modules", + ".nyc_output", + "coverage", + ".git", + ".github", + "features" + ], + "reporter": ["html", "lcov", "text", "text-summary"], + "report-dir": "coverage" +} diff --git a/README.md b/README.md index 5b5afa7..a6115e9 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,18 @@ A property that holds a uuid as a primary key. It is automatically created if no [Documentation](https://monolithst.github.io/functional-models/functions/index.properties.PrimaryKeyUuidProperty.html) +#### UuidProperty + +A property that holds a uuid value. Can be used for any uuid field, not just primary keys. If `autoNow` is set to true, a uuid will be automatically generated if not provided. Validates the value as a uuid if required, or allows undefined if not required. + +[Documentation](https://monolithst.github.io/functional-models/functions/index.properties.UuidProperty.html) + +#### ForeignKeyProperty + +A property for representing a foreign key to another model in ORM-backed models. By default, it uses a uuid type, but can be configured to use a string or integer type via the `dataType` config. Provides utility methods to get the referenced id and model. Useful for defining foreign key relationships in a database-agnostic way. + +[Documentation](https://monolithst.github.io/functional-models/functions/index.orm.properties.ForeignKeyProperty.html) + #### ModelReferenceProperty A property that holds a reference to another model instance. (In database-speak a foreign key). When code requests the value for this property, it is fetched and returns an object. However, when `.toObj()` is called on the model, this reference turns into a id. (number or string) diff --git a/package-lock.json b/package-lock.json index 426aec0..23d27c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "functional-models", - "version": "3.0.3", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "functional-models", - "version": "3.0.3", + "version": "3.1.0", "license": "GPLV3", "dependencies": { "async-lock": "^1.3.0", @@ -25,12 +25,13 @@ "@types/chai": "^5.0.1", "@types/chai-as-promised": "^7.1.4", "@types/lodash": "^4.14.176", - "@types/mocha": "^9.0.0", + "@types/mocha": "^9.1.1", "@types/node": "^22.10.7", "@types/proxyquire": "^1.3.28", "@types/sinon": "^10.0.6", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", + "c8": "^10.1.3", "chai": "^5.1.2", "chai-as-promised": "^7.1.1", "cz-conventional-changelog": "^3.3.0", @@ -313,6 +314,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1720,6 +1731,13 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1742,7 +1760,8 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "22.10.7", @@ -2466,6 +2485,125 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -8577,6 +8715,28 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 5cc64b6..7e2f7e2 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "functional-models", - "version": "3.0.16", + "version": "3.1.0", "description": "Functional models is ooey gooey framework for building and using awesome models EVERYWHERE.", "main": "index.js", "types": "index.d.ts", "scripts": { "mocha": "mocha -r tsx", "test": "mocha -r tsx --extensions ts,tsx 'test/**/*.{ts,tsx}'", - "test:coverage": "TS_NODE_PROJECT=tsconfig.test.json nyc npm run test", + "test:coverage": "c8 --all --reporter cobertura --reporter text --reporter lcov --reporter html npm run test", "test:watch": "nodemon -e '*' --watch test --watch src --exec npm run test:coverage", "commit": "cz", "feature-tests": "./node_modules/.bin/cucumber-js -p default", @@ -45,34 +45,6 @@ } }, "homepage": "https://github.com/monolithst/functional-models#readme", - "nyc": { - "branches": 90, - "lines": 100, - "functions": 100, - "statements": 100, - "extends": "@istanbuljs/nyc-config-typescript", - "check-coverage": true, - "all": true, - "include": [ - "src/**/!(*.test.*).[tj]s?(x)" - ], - "exclude": [ - "src/_tests_/**/*.*", - "node_modules", - ".nyc_output", - "coverage", - ".git", - ".github", - "features" - ], - "reporter": [ - "html", - "lcov", - "text", - "text-summary" - ], - "report-dir": "coverage" - }, "devDependencies": { "@cucumber/cucumber": "^11.0.0", "@date-fns/utc": "^1.2.0", @@ -84,12 +56,13 @@ "@types/chai": "^5.0.1", "@types/chai-as-promised": "^7.1.4", "@types/lodash": "^4.14.176", - "@types/mocha": "^9.0.0", + "@types/mocha": "^9.1.1", "@types/node": "^22.10.7", "@types/proxyquire": "^1.3.28", "@types/sinon": "^10.0.6", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", + "c8": "^10.1.3", "chai": "^5.1.2", "chai-as-promised": "^7.1.1", "cz-conventional-changelog": "^3.3.0", diff --git a/src/orm/properties.ts b/src/orm/properties.ts index df195e1..a50e91e 100644 --- a/src/orm/properties.ts +++ b/src/orm/properties.ts @@ -1,7 +1,20 @@ import merge from 'lodash/merge' import identity from 'lodash/identity' -import { DateValueType, PropertyConfig, Arrayable, DataValue } from '../types' -import { DatetimeProperty } from '../properties' +import { + DateValueType, + PropertyConfig, + Arrayable, + DataValue, + MaybeFunction, + ModelType, + DataDescription, +} from '../types' +import { + DatetimeProperty, + IntegerProperty, + TextProperty, + UuidProperty, +} from '../properties' import { unique } from './validation' import { OrmPropertyConfig } from './types' @@ -20,6 +33,62 @@ const LastModifiedDateProperty = ( return DatetimeProperty(config, additionalMetadata) } +/** + * A property that represents a foreign key to another model. + * By default it is a "uuid" type, but if you want to use an arbitrary string, or an integer type you can set the `dataType` property. + * @interface + */ +type ForeignKeyPropertyConfig = + PropertyConfig & + Readonly<{ + /** + * Sets the type of the foreign key. + * @default 'uuid' + */ + dataType?: 'uuid' | 'string' | 'integer' + }> + +/** + * A property that represents a foreign key to another model in a database. + * By default it is a "uuid" type, but if you want to use an arbitrary string, or an integer type you can set the `dataType` property. + * @param config - Additional configurations. + */ +const ForeignKeyProperty = < + TValue extends string | number, + TModel extends DataDescription, +>( + model: MaybeFunction>, + config: ForeignKeyPropertyConfig = {} +) => { + const _getModel = () => { + if (typeof model === 'function') { + return model() + } + return model + } + + const _getProperty = () => { + if (config.dataType === 'uuid') { + return UuidProperty( + merge(config as ForeignKeyPropertyConfig, { + autoNow: false, + }) + ) + } + if (config.dataType === 'integer') { + return IntegerProperty(config as ForeignKeyPropertyConfig) + } + return TextProperty(config as ForeignKeyPropertyConfig) + } + const property = _getProperty() + return merge(property, { + getReferencedId: (instanceValues: TValue) => { + return instanceValues + }, + getReferencedModel: _getModel, + }) +} + /** * Creates an orm based property config. * @param config - Additional configurations. @@ -35,4 +104,4 @@ const ormPropertyConfig = >( }) } -export { ormPropertyConfig, LastModifiedDateProperty } +export { ormPropertyConfig, LastModifiedDateProperty, ForeignKeyProperty } diff --git a/src/properties.ts b/src/properties.ts index df47148..f537997 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -8,6 +8,7 @@ import { maxNumber, meetsRegex, minNumber, + optionalValidator, referenceTypeMatch, } from './validation' import { @@ -520,6 +521,37 @@ const PrimaryKeyUuidProperty = ( additionalMetadata ) +/** + * A property that has a uuid. + * This property has required on it. + * If you want it to automatically create a uuid add { autoNow: true } to the config. + * @param config - Additional configurations. + * @param additionalMetadata - Any additional metadata. + */ +const UuidProperty = ( + config: PropertyConfig = {}, + additionalMetadata = {} +) => + Property( + PropertyType.UniqueId, + merge(config, { + isString: true, + validators: mergeValidators( + config, + config.required ? isValidUuid : optionalValidator(isValidUuid) + ), + lazyLoadMethod: (value: Arrayable) => { + if (!value) { + if (config.autoNow) { + return createUuid() + } + } + return value + }, + }), + additionalMetadata + ) + /** * A property that has a reference to another model instance. A "Foreign Key" if you will. * For full functionality a {@link ModelInstanceFetcher} must be provided in the config. @@ -871,4 +903,5 @@ export { YearProperty, PrimaryKeyUuidProperty, SingleTypeArrayProperty, + UuidProperty, } diff --git a/test/src/index.test.ts b/test/src/index.test.ts new file mode 100644 index 0000000..4f80d9b --- /dev/null +++ b/test/src/index.test.ts @@ -0,0 +1,9 @@ +import { assert } from 'chai' +import { describe, it } from 'mocha' + +describe('/src/index.ts', () => { + it('should load', async () => { + const x = await import('../../src/index') + assert.isOk(x) + }) +}) diff --git a/test/src/orm/properties.test.ts b/test/src/orm/properties.test.ts index e111196..1460f1b 100644 --- a/test/src/orm/properties.test.ts +++ b/test/src/orm/properties.test.ts @@ -4,7 +4,10 @@ import { PropertyValidatorComponent, unique, ormPropertyConfig, + ForeignKeyProperty, + LastModifiedDateProperty, } from '../../../src' +import { PropertyType } from '../../../src/types' describe('/src/orm/properties.ts', () => { describe('#ormPropertyConfig()', () => { @@ -42,4 +45,71 @@ describe('/src/orm/properties.ts', () => { assert.equal(actual, expected) }) }) + + describe('#ForeignKeyProperty()', () => { + // Minimal valid ModelType mock + const DummyModel: OrmModel = { + getName: () => 'Dummy', + getModelDefinition: () => ({ + pluralName: 'Dummies', + namespace: 'test', + properties: {}, + primaryKeyName: 'id', + modelValidators: [], + singularName: 'Dummy', + displayName: 'Dummy', + description: 'A dummy model', + }), + getPrimaryKey: () => 'id', + getApiInfo: () => ({ + noPublish: false, + onlyPublish: [], + // @ts-ignore + rest: {}, + createOnlyOne: false, + }), + create: () => ({}), + } + const DummyModelFn = () => DummyModel + + it('should use UuidProperty when dataType is uuid', () => { + const prop = ForeignKeyProperty(DummyModel, { dataType: 'uuid' }) + assert.equal(prop.getPropertyType(), PropertyType.UniqueId) + // @ts-ignore + assert.equal(prop.getConfig().dataType, 'uuid') + }) + + it('should use IntegerProperty when dataType is integer', () => { + const prop = ForeignKeyProperty(DummyModel, { dataType: 'integer' }) + assert.equal(prop.getPropertyType(), PropertyType.Integer) + // @ts-ignore + assert.equal(prop.getConfig().dataType, 'integer') + }) + + it('should use TextProperty when dataType is string', () => { + const prop = ForeignKeyProperty(DummyModel, { dataType: 'string' }) + assert.equal(prop.getPropertyType(), PropertyType.Text) + // @ts-ignore + assert.equal(prop.getConfig().dataType, 'string') + }) + + it('should resolve model if passed as a function', () => { + const prop = ForeignKeyProperty(DummyModelFn, { dataType: 'uuid' }) + assert.deepEqual(prop.getReferencedModel(), DummyModel) + }) + + it('getReferencedId should return the instance value', () => { + const prop = ForeignKeyProperty(DummyModel, { dataType: 'uuid' }) + assert.equal(prop.getReferencedId('abc-123'), 'abc-123') + }) + }) + + describe('#LastModifiedDateProperty()', () => { + it('should return a property with lastModifiedUpdateMethod', () => { + const prop = LastModifiedDateProperty() + assert.isFunction(prop.lastModifiedUpdateMethod) + const date = prop.lastModifiedUpdateMethod() + assert.instanceOf(date, Date) + }) + }) }) diff --git a/test/src/orm/query.test.ts b/test/src/orm/query.test.ts index e485372..85acaf9 100644 --- a/test/src/orm/query.test.ts +++ b/test/src/orm/query.test.ts @@ -20,6 +20,7 @@ import { threeitize, validateOrmSearch, } from '../../../src' +import flow from 'lodash/flow' describe('/src/orm/query.ts', () => { describe('#validateQueryTokens()', () => { @@ -756,5 +757,82 @@ describe('/src/orm/query.ts', () => { assert.deepEqual(actual, expected) }) }) + it('should build a query with four ORed string properties and .take(20) at the end, with no trailing OR', () => { + const search = 'search-value' + const searchProps = ['firstName', 'lastName', 'email', 'displayName'] + // Compose the query using the same pattern as the bug report + const query = searchProps.reduce((qb, prop, i) => { + const isLast = i === searchProps.length - 1 + const q = qb.property(prop, search, { caseSensitive: false }) + return isLast ? q : q.or() + }, queryBuilder()) + const compiled = query.take(20).compile() + // The expected query array + const expectedQuery = [ + property('firstName', search, { caseSensitive: false }), + 'OR', + property('lastName', search, { caseSensitive: false }), + 'OR', + property('email', search, { caseSensitive: false }), + 'OR', + property('displayName', search, { caseSensitive: false }), + ] + // Should not have a trailing OR + assert.deepEqual(compiled.query, expectedQuery) + // Should have take: 20 + assert.equal(compiled.take, 20) + }) + it('should build a query with .take(20) directly on queryBuilder() and get an empty query with take', () => { + const compiled = queryBuilder().take(20).compile() + assert.deepEqual(compiled, { query: [], take: 20 }) + }) + it('should build a query with .take(20) at the end (flow pattern)', () => { + const search = 'search-value' + const searchProps = ['firstName', 'lastName', 'email', 'displayName'] + const expectedQuery = [ + property('firstName', search, { caseSensitive: false }), + 'OR', + property('lastName', search, { caseSensitive: false }), + 'OR', + property('email', search, { caseSensitive: false }), + 'OR', + property('displayName', search, { caseSensitive: false }), + ] + + const query = flow( + searchProps.map((prop, i) => qb => { + const isLast = i === searchProps.length - 1 + const q = qb.property(prop, search, { caseSensitive: false }) + return isLast ? q : q.or() + }) + )(queryBuilder()) + .take(20) + .compile() + assert.deepEqual(query.query, expectedQuery) + assert.equal(query.take, 20) + }) + + it('should build a query with .take(20) at the start (flow pattern)', () => { + const search = 'search-value' + const searchProps = ['firstName', 'lastName', 'email', 'displayName'] + const expectedQuery = [ + property('firstName', search, { caseSensitive: false }), + 'OR', + property('lastName', search, { caseSensitive: false }), + 'OR', + property('email', search, { caseSensitive: false }), + 'OR', + property('displayName', search, { caseSensitive: false }), + ] + const query = flow( + searchProps.map((prop, i) => qb => { + const isLast = i === searchProps.length - 1 + const q = qb.property(prop, search, { caseSensitive: false }) + return isLast ? q : q.or() + }) + )(queryBuilder().take(20)).compile() + assert.deepEqual(query.query, expectedQuery) + assert.equal(query.take, 20) + }) }) }) diff --git a/test/src/utils.test.ts b/test/src/utils.test.ts index 8d5a3be..a04e8cf 100644 --- a/test/src/utils.test.ts +++ b/test/src/utils.test.ts @@ -14,7 +14,7 @@ describe('/src/utils.ts', () => { describe('#memoizeAsync()', () => { it('should only call the method passed in once even after two calls', async () => { const method = sinon.stub().returns('hello-world') - const instance = memoizeAsync(method) + const instance = memoizeAsync(method) await instance() await instance() sinon.assert.calledOnce(method)