Тесты работают, TS билдится
This commit is contained in:
parent
e08d9757bb
commit
9f3a64d76f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
7
.idea/compiler.xml
Normal file
7
.idea/compiler.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="nodeInterpreterTextField" value="$USER_HOME$/Downloads/node-v14.16.0-linux-x64/bin/node" />
|
||||
<option name="recompileOnChanges" value="true" />
|
||||
</component>
|
||||
</project>
|
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
12
.idea/ivanovna.orm.iml
Normal file
12
.idea/ivanovna.orm.iml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/tests" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/jsLibraryMappings.xml
Normal file
6
.idea/jsLibraryMappings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="file://$PROJECT_DIR$" libraries="{Node.js Core}" />
|
||||
</component>
|
||||
</project>
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ivanovna.orm.iml" filepath="$PROJECT_DIR$/.idea/ivanovna.orm.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
7
.idea/prettier.xml
Normal file
7
.idea/prettier.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
<option name="myRunOnReformat" value="true" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
28
jest.config.js
Normal file
28
jest.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = {
|
||||
roots: ['<rootDir>/src/tests/'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
testRunner: 'jest-jasmine2',
|
||||
reporters: ['default', 'jest-teamcity'],
|
||||
setupFiles: ['<rootDir>/src/tests/conf/setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/tests/conf/setup-after-env.ts'],
|
||||
bail: 1,
|
||||
verbose: true,
|
||||
testSequencer: '<rootDir>/src/tests/conf/sequencer.js',
|
||||
testTimeout: 60000,
|
||||
coverageProvider: 'v8',
|
||||
coverageReporters: ['json', 'lcov', 'text', 'clover', 'teamcity'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
functions: 60,
|
||||
lines: 60,
|
||||
statements: 60,
|
||||
},
|
||||
},
|
||||
};
|
29
jest.config.ts
Normal file
29
jest.config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export default {
|
||||
roots: ['<rootDir>/src/tests/'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
testRunner: 'jest-jasmine2',
|
||||
reporters: ['default', 'jest-teamcity'],
|
||||
|
||||
setupFiles: ['<rootDir>/src/tests/conf/setup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/tests/conf/setup-after-env.ts'],
|
||||
|
||||
bail: 1,
|
||||
verbose: true,
|
||||
testSequencer: '<rootDir>/src/tests/conf/sequencer.js',
|
||||
testTimeout: 60000,
|
||||
|
||||
coverageProvider: 'v8',
|
||||
coverageReporters: ['json', 'lcov', 'text', 'clover', 'teamcity'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 60,
|
||||
functions: 60,
|
||||
lines: 60,
|
||||
statements: 60,
|
||||
},
|
||||
},
|
||||
}
|
17074
package-lock.json
generated
Normal file
17074
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
58
package.json
Normal file
58
package.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "ivna-orm",
|
||||
"version": "1.0.0",
|
||||
"description": "Mini ORM for convenience",
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node --no-warnings node_modules/.bin/jest --runInBand --forceExit",
|
||||
"prepare": "npm run lint && npm test && npm run build",
|
||||
"lint": "xo --space=4",
|
||||
"build": "tsc -b -v"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.archive.systems/ivanovna/ivanovna.orm.git"
|
||||
},
|
||||
"keywords": [
|
||||
"ivanovna",
|
||||
"ivna",
|
||||
"orm"
|
||||
],
|
||||
"author": "Nikita Orlov",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@jest/test-sequencer": "^27.2.3",
|
||||
"@types/bson": "^4.2.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^14.17.19",
|
||||
"@types/rfdc": "^1.2.0",
|
||||
"chai": "^4.3.4",
|
||||
"jasmine": "^3.9.0",
|
||||
"jasmine-fail-fast": "^2.0.1",
|
||||
"jest": "^27.2.3",
|
||||
"jest-teamcity": "^1.10.0",
|
||||
"nanoid": "^3.1.28",
|
||||
"prettier": "^2.4.1",
|
||||
"rfdc": "^1.3.0",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.4.3",
|
||||
"xo": "^0.44.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongodb": "^4.1.2",
|
||||
"mongodb-client-encryption": "^1.2.7"
|
||||
},
|
||||
"prettier": {
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 110,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
0
spec/app/storage/storage.d.ts
vendored
Normal file
0
spec/app/storage/storage.d.ts
vendored
Normal file
0
spec/app/storage/storage.d.ts.map
Normal file
0
spec/app/storage/storage.d.ts.map
Normal file
0
spec/data.d.ts
vendored
Normal file
0
spec/data.d.ts
vendored
Normal file
1
spec/data.d.ts.map
Normal file
1
spec/data.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAEA,UAAU,YAAY;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACrB;AAED,8BAAsB,IAAI;IAEtB,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;IAOnD,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;IAElB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG;IAI3B,UAAU,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAOnC,QAAQ,CAAC,OAAO,GAAE,MAAM,EAAO,GAAG,MAAM;IAqBxC,QAAQ,CAAC,OAAO,IAAI,MAAM;CAC7B"}
|
0
spec/entity.d.ts
vendored
Normal file
0
spec/entity.d.ts
vendored
Normal file
1
spec/entity.d.ts.map
Normal file
1
spec/entity.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,oBAAY,iBAAiB,CAAC,CAAC,SAAS,IAAI,IAAI;IAC5C,KAAK,IAAI,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;CAC5B,CAAA;AAED,qBAAa,0BAA2B,SAAQ,KAAK;CAAG;AAExD,8BAAsB,MAAM,CAAC,CAAC,SAAS,IAAI;IACvC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,IAAI,CAAC;IAEpB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAA;IAClB,OAAO,CAAC,IAAI,CAAa;IAEzB,IAAI,IAAI,IAAI,CAAC,CAEZ;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM,EAEpB;gBAEW,IAAI,CAAC,EAAE,CAAC;IAUpB,UAAU,IAAI,GAAG;IASjB,WAAW,IAAI,OAAO;IAItB,QAAQ;CAGX"}
|
0
spec/repo.d.ts
vendored
Normal file
0
spec/repo.d.ts
vendored
Normal file
1
spec/repo.d.ts.map
Normal file
1
spec/repo.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../src/repo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,qBAAa,qBAAsB,SAAQ,KAAK;CAAG;AACnD,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAE/C,8BAAsB,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,CAAC;IAC5C,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC3B,SAAS,CAAC,OAAO,EAAE,CAAC,CAAA;IAEpB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAI;IAC5B,SAAS,CAAC,OAAO,EAAE,MAAM,CAAI;IAK7B,QAAQ,CAAC,MAAM,IAAI,CAAC;IAKpB,QAAQ,CAAC,IAAI,IAAI,MAAM;IAGvB,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,CAAA;gBAEnB,OAAO,EAAE,OAAO;IAM5B,gBAAgB;IAShB,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAoBvC,aAAa,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAanE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAc5D,KAAK,CAAC,KAAK,SAAI,EAAE,MAAM,SAAI;IAMrB,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAItC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAOxD"}
|
0
spec/storage.d.ts
vendored
Normal file
0
spec/storage.d.ts
vendored
Normal file
1
spec/storage.d.ts.map
Normal file
1
spec/storage.d.ts.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;IACjC,IAAI,CAAC,MAAM,EAAE,EAAE,GAAG,aAAa,CAAA;IAC/B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAA;IACnC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;CAC5B;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;AACxC,qBAAa,YAAa,SAAQ,UAAU;CAAG;AAE/C,8BAAsB,OAAO;IACzB,SAAS,CAAC,IAAI,EAAE,MAAM,CAAA;gBAEV,GAAG,EAAE,MAAM;IAIvB,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAElE,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE3E,QAAQ,CAAC,aAAa,IAAI,cAAc;IAExC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE7D,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAClG;AAED,8BAAsB,cAAc;IAChC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAC9D"}
|
50
src/app/data.ts
Normal file
50
src/app/data.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import rfdc from 'rfdc'
|
||||
|
||||
interface ValuesObject {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export abstract class Data {
|
||||
static assign(vo: Data, values: ValuesObject): Data {
|
||||
for (let key of Object.getOwnPropertyNames(values)) {
|
||||
vo[key] = values[key]
|
||||
}
|
||||
return vo
|
||||
}
|
||||
|
||||
[key: string]: any
|
||||
|
||||
set(key: string, value: any) {
|
||||
this[key] = value
|
||||
}
|
||||
|
||||
fromObject(obj: ValuesObject): void {
|
||||
for (let key of Object.getOwnPropertyNames(obj)) {
|
||||
if (key === '_id') continue
|
||||
this[key] = obj[key]
|
||||
}
|
||||
}
|
||||
|
||||
toObject(exclude: string[] = []): object {
|
||||
const obj = {} as ValuesObject
|
||||
for (let key of Object.getOwnPropertyNames(this)) {
|
||||
if (key.startsWith('__')) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (exclude.includes(key)) continue
|
||||
|
||||
const type = typeof this[key]
|
||||
if (type === 'object') {
|
||||
obj[key] = rfdc()(this[key])
|
||||
} else if (['function', 'undefined'].indexOf(type) >= 0) {
|
||||
// ...
|
||||
} else {
|
||||
obj[key] = this[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
abstract uniqKey(): string
|
||||
}
|
56
src/app/entity.ts
Normal file
56
src/app/entity.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Data } from './data'
|
||||
import { Repo } from './repo'
|
||||
import { Storage } from './storage'
|
||||
|
||||
export type EntityConstructor<T extends Data> = {
|
||||
new (data?: T): Entity<T>
|
||||
}
|
||||
|
||||
export class ErrEntityHasNoUniqKeyValue extends Error {}
|
||||
|
||||
export abstract class Entity<T extends Data> {
|
||||
abstract _getRepo(storage: Storage): Repo<Entity<T>>
|
||||
abstract _getVO(): T
|
||||
|
||||
protected _data: T
|
||||
private __id: string = '' // storage inner id
|
||||
|
||||
get data(): T {
|
||||
return this._data
|
||||
}
|
||||
|
||||
get _id(): string {
|
||||
return this.__id
|
||||
}
|
||||
|
||||
set _id(value: string) {
|
||||
this.__id = value
|
||||
}
|
||||
|
||||
constructor(data?: T) {
|
||||
if (data) {
|
||||
const newData = this._getVO()
|
||||
newData.fromObject(data.toObject(['_id']))
|
||||
this._data = newData
|
||||
} else {
|
||||
this._data = this._getVO()
|
||||
}
|
||||
}
|
||||
|
||||
getUniqKey(): any {
|
||||
const id = this._data[this._data.uniqKey()]
|
||||
if (id === null || id === undefined || id === '') {
|
||||
throw new ErrEntityHasNoUniqKeyValue()
|
||||
} else {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
isPersisted(): boolean {
|
||||
return this.__id !== ''
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.getUniqKey()
|
||||
}
|
||||
}
|
116
src/app/repo.ts
Normal file
116
src/app/repo.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Entity } from './entity'
|
||||
import { Storage } from './storage'
|
||||
|
||||
export class ErrFoundNotUniqEntity extends Error {}
|
||||
export class ErrEntityNotFound extends Error {}
|
||||
|
||||
export abstract class Repo<T extends Entity<any>> {
|
||||
protected _storage: Storage
|
||||
protected _entity: T
|
||||
|
||||
protected _limit: number = 0
|
||||
protected _offset: number = 0
|
||||
|
||||
/**
|
||||
* Возвращает объект соотв. сущности, например new App()
|
||||
*/
|
||||
abstract Entity(): T
|
||||
|
||||
/**
|
||||
* Возвращает название коллекции/таблицы/... хранящей записи соотв. сущностей
|
||||
*/
|
||||
abstract Name(): string
|
||||
|
||||
// @ts-ignore
|
||||
_transformer: (obj: any) => any
|
||||
|
||||
constructor(storage: Storage) {
|
||||
this._storage = storage
|
||||
this._entity = this.Entity()
|
||||
this.resetTransformer()
|
||||
}
|
||||
|
||||
resetTransformer() {
|
||||
this._transformer = function (obj: any): any {
|
||||
const entity = this.Entity()
|
||||
entity.data.fromObject(obj)
|
||||
entity._id = obj._id
|
||||
return entity
|
||||
}
|
||||
}
|
||||
|
||||
get storage(): Storage {
|
||||
return this._storage
|
||||
}
|
||||
|
||||
async save(entity: T): Promise<this> {
|
||||
const uniqKey = entity.data.uniqKey()
|
||||
entity._id = await this._storage.save(this.Name(), uniqKey, entity.data.toObject())
|
||||
return this
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<T | null> {
|
||||
const uniqKey = this.Entity().data.uniqKey()
|
||||
const cursor = await this._storage.find(this.Name(), { [uniqKey]: id })
|
||||
const entryList = await cursor.toArray()
|
||||
if (entryList.length > 1) {
|
||||
throw new ErrFoundNotUniqEntity(
|
||||
`found few (${entryList.length}) entries in ${this.Name()} for id ${id}`
|
||||
)
|
||||
}
|
||||
if (entryList.length == 0) {
|
||||
throw new ErrEntityNotFound(`not found entry in [${this.Name()}] for id [${id}]`)
|
||||
}
|
||||
|
||||
return this._transformer(entryList[0])
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
async _findByParams(params: {}, limit?: number, order?: {}): Promise<T[]> {
|
||||
const cursor = await this._storage.find(this.Name(), params)
|
||||
if (limit && limit > 0) await cursor.limit(limit)
|
||||
if (order) await cursor.sort(order)
|
||||
const data = await cursor.toArray()
|
||||
const list = []
|
||||
for (let item of data) {
|
||||
list.push(this._transformer(item))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
async findMany(params: object, order?: object): Promise<T[]> {
|
||||
const cursor = await this._storage.find(this.Name(), params)
|
||||
if (this._offset > 0) await cursor.skip(this._offset)
|
||||
if (this._limit > 0) await cursor.limit(this._limit)
|
||||
if (order) await cursor.sort(order)
|
||||
const data = await cursor.toArray()
|
||||
const list = []
|
||||
for (let item of data) {
|
||||
list.push(this._transformer(item))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
shift(limit = 0, offset = 0) {
|
||||
this._limit = limit
|
||||
this._offset = offset
|
||||
return this
|
||||
}
|
||||
|
||||
async count(query?: object): Promise<number> {
|
||||
return this._storage.count(this.Name(), query)
|
||||
}
|
||||
|
||||
async remove(entity: T, silent = true): Promise<this> {
|
||||
const idName = entity.data.uniqKey()
|
||||
const id = entity.getUniqKey()
|
||||
const ok = await this._storage.remove(this.Name(), idName, id)
|
||||
if (!silent && !ok) throw new ErrEntityNotFound(`can not remove entry in ${this.Name()} for id ${id}`)
|
||||
return this
|
||||
}
|
||||
}
|
32
src/app/storage.ts
Normal file
32
src/app/storage.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export interface StorageCursor {
|
||||
limit(num: number): StorageCursor
|
||||
sort(params: {}): StorageCursor
|
||||
skip(offset: number): StorageCursor
|
||||
toArray(): Promise<any[]>
|
||||
}
|
||||
|
||||
export class ErrStorage extends Error {}
|
||||
export class ErrNoSession extends ErrStorage {}
|
||||
|
||||
export abstract class Storage {
|
||||
protected _dsn: string
|
||||
|
||||
constructor(dsn: string) {
|
||||
this._dsn = dsn
|
||||
}
|
||||
|
||||
abstract find(name: string, query: Object): Promise<StorageCursor>
|
||||
|
||||
abstract save(name: string, uniqKey: string, data: Object): Promise<string>
|
||||
|
||||
abstract createSession(): StorageSession
|
||||
|
||||
abstract count(name: string, query?: object): Promise<number>
|
||||
|
||||
abstract remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean>
|
||||
}
|
||||
|
||||
export abstract class StorageSession {
|
||||
abstract start(options?: {}): Promise<void>
|
||||
abstract commit(fn: () => any, options?: {}): Promise<void>
|
||||
}
|
105
src/app/storage/storage.ts
Normal file
105
src/app/storage/storage.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Storage, StorageCursor, StorageSession } from '../storage'
|
||||
import { ClientSession, MongoClient, TransactionOptions } from 'mongodb'
|
||||
import { ErrEntityHasNoUniqKeyValue } from '../entity'
|
||||
|
||||
export class MongoStorage extends Storage {
|
||||
private _client: MongoClient
|
||||
_session: ClientSession
|
||||
|
||||
constructor(dsn: string) {
|
||||
super(dsn)
|
||||
this._client = new MongoClient(dsn)
|
||||
this._session = null
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this._connect()
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
await this._client.connect()
|
||||
}
|
||||
|
||||
async find(collectionName: string, query: object): Promise<StorageCursor> {
|
||||
await this._connect()
|
||||
const coll = await this._client.db().collection(collectionName)
|
||||
return coll.find(query)
|
||||
}
|
||||
|
||||
async count(collectionName: string, query: object): Promise<number> {
|
||||
await this._connect()
|
||||
const coll = await this._client.db().collection(collectionName)
|
||||
return coll.countDocuments(query)
|
||||
}
|
||||
|
||||
async save(collectionName: string, uniqKey: string, data: object): Promise<string> {
|
||||
await this._connect()
|
||||
const id = data[uniqKey]
|
||||
const coll = await this._client.db().collection(collectionName)
|
||||
if (id !== null && id !== undefined) {
|
||||
const filter = { [uniqKey]: id }
|
||||
const result = await coll.findOneAndReplace(filter, data, { upsert: true })
|
||||
if (result.lastErrorObject) {
|
||||
if (result.lastErrorObject.updatedExisting) {
|
||||
return result.value._id
|
||||
} else {
|
||||
return result.lastErrorObject.upserted
|
||||
}
|
||||
} else {
|
||||
throw new TypeError(`can not save data to ${collectionName} with result ${result}`)
|
||||
}
|
||||
} else {
|
||||
// нельзя сохранить сущность без значения уникального ключа
|
||||
// const result = await coll.insertOne(data)
|
||||
// return result.insertedId._id.toHexString()
|
||||
throw new ErrEntityHasNoUniqKeyValue()
|
||||
}
|
||||
}
|
||||
|
||||
async _getCollection(name: string) {
|
||||
await this._connect()
|
||||
return this._client.db().collection(name)
|
||||
}
|
||||
|
||||
createSession(): MongoStorageSession {
|
||||
return new MongoStorageSession(this._client)
|
||||
}
|
||||
|
||||
get client(): MongoClient {
|
||||
return this._client
|
||||
}
|
||||
|
||||
async remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
|
||||
const coll = await this._client.db().collection(collectionName)
|
||||
const result = await coll.deleteOne({ [uniqKeyName]: uniqKey })
|
||||
return Promise.resolve(result.acknowledged)
|
||||
}
|
||||
}
|
||||
|
||||
export class MongoStorageSession extends StorageSession {
|
||||
_client: MongoClient
|
||||
_session: ClientSession
|
||||
|
||||
constructor(client: MongoClient) {
|
||||
super()
|
||||
this._client = client
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this._session) {
|
||||
await this._session.endSession()
|
||||
}
|
||||
|
||||
this._session = this._client.startSession()
|
||||
}
|
||||
|
||||
async commit(fn, options?: TransactionOptions) {
|
||||
try {
|
||||
await this._session.withTransaction(fn, options)
|
||||
} catch (e) {
|
||||
throw e
|
||||
}
|
||||
|
||||
await this._session.endSession()
|
||||
}
|
||||
}
|
13
src/tests/conf/sequencer.js
Normal file
13
src/tests/conf/sequencer.js
Normal file
@ -0,0 +1,13 @@
|
||||
const Sequencer = require('@jest/test-sequencer').default;
|
||||
|
||||
class CustomSequencer extends Sequencer {
|
||||
sort(tests) {
|
||||
// Test structure information
|
||||
// https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21
|
||||
const copyTests = Array.from(tests);
|
||||
// @ts-ignore
|
||||
return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CustomSequencer;
|
10
src/tests/conf/setup-after-env.ts
Normal file
10
src/tests/conf/setup-after-env.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const failFast = require("jasmine-fail-fast");
|
||||
|
||||
// Добавляем нормальный fail fast
|
||||
// https://github.com/facebook/jest/issues/2867
|
||||
|
||||
//if (process.argv.includes("--bail")) {
|
||||
// @ts-ignore
|
||||
const jasmineEnv = jasmine.getEnv();
|
||||
jasmineEnv.addReporter(failFast.init());
|
||||
//}
|
0
src/tests/conf/setup.ts
Normal file
0
src/tests/conf/setup.ts
Normal file
48
src/tests/data.test.ts
Normal file
48
src/tests/data.test.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import assert from 'assert'
|
||||
import { Data } from '../app/data'
|
||||
|
||||
export class ItemData extends Data {
|
||||
uniqKey(): string {
|
||||
return 'a'
|
||||
}
|
||||
|
||||
a: number = 0
|
||||
b: number
|
||||
c: { d: number; e?: number } = { d: 1 }
|
||||
__f: number = 0
|
||||
sum() {
|
||||
return this.a + this.c.d + this.c.e
|
||||
}
|
||||
}
|
||||
|
||||
describe('Data', function () {
|
||||
describe('#toObject', function () {
|
||||
it('should return object', function () {
|
||||
const data = new ItemData()
|
||||
data.a = 1
|
||||
data.c = { d: 2, e: undefined }
|
||||
const obj = data.toObject()
|
||||
|
||||
assert.strictEqual('a' in obj, true)
|
||||
assert.strictEqual('b' in obj, false)
|
||||
assert.strictEqual('c' in obj, true)
|
||||
assert.strictEqual('d' in obj['c'], true)
|
||||
assert.strictEqual('e' in obj['c'], true)
|
||||
assert.strictEqual('__f' in obj, false)
|
||||
assert.strictEqual('sum' in obj, false)
|
||||
})
|
||||
})
|
||||
describe('#fromObject', () => {
|
||||
it('should return data object', () => {
|
||||
const dataOrig = new ItemData()
|
||||
dataOrig.a = 1
|
||||
dataOrig.c = { d: 2, e: undefined }
|
||||
const obj = dataOrig.toObject()
|
||||
|
||||
const dataRecovered = new ItemData()
|
||||
dataRecovered.fromObject(obj)
|
||||
|
||||
assert.notStrictEqual(dataOrig, dataRecovered)
|
||||
})
|
||||
})
|
||||
})
|
50
src/tests/repository.test.ts
Normal file
50
src/tests/repository.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { ItemData } from './data.test'
|
||||
import { Entity } from '../app/entity'
|
||||
import { Storage } from '../app/storage'
|
||||
import { Repo } from '../app/repo'
|
||||
import { MapStorage } from './storage.test'
|
||||
import { Data } from '../app/data'
|
||||
import assert from 'assert'
|
||||
|
||||
class Item extends Entity<ItemData> {
|
||||
_getVO(): ItemData {
|
||||
return new ItemData()
|
||||
}
|
||||
|
||||
protected _data: ItemData
|
||||
|
||||
getA(): number {
|
||||
return this._data.a
|
||||
}
|
||||
getC(): { d: number; e?: number } {
|
||||
return this._data.c
|
||||
}
|
||||
|
||||
_getRepo(storage: Storage): ItemRepository {
|
||||
return new ItemRepository(storage)
|
||||
}
|
||||
}
|
||||
|
||||
class ItemRepository extends Repo<Item> {
|
||||
Name() {
|
||||
return 'items'
|
||||
}
|
||||
Entity() {
|
||||
return new Item()
|
||||
}
|
||||
}
|
||||
|
||||
describe('ItemRepository', function () {
|
||||
describe('map storage save', function () {
|
||||
const storage = new MapStorage('')
|
||||
const repo = new ItemRepository(storage)
|
||||
|
||||
it('should return entity', async () => {
|
||||
const dt = Data.assign(new ItemData(), { a: 1, b: 2, c: { d: 3 } })
|
||||
const item = new Item(dt as ItemData)
|
||||
await repo.save(item)
|
||||
const itemFromRepo = await repo.findById('1')
|
||||
assert.notStrictEqual(item.data, itemFromRepo.data)
|
||||
})
|
||||
})
|
||||
})
|
132
src/tests/storage.test.ts
Normal file
132
src/tests/storage.test.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { strict as assert } from 'assert'
|
||||
import { Storage, StorageCursor, StorageSession } from '../app/storage'
|
||||
|
||||
export class MapCursor implements StorageCursor {
|
||||
_map: Map<any, any>
|
||||
_limit = 0
|
||||
|
||||
constructor(map: Map<any, any>) {
|
||||
this._map = map
|
||||
}
|
||||
|
||||
limit(num: number): StorageCursor {
|
||||
this._limit = num
|
||||
return this
|
||||
}
|
||||
|
||||
sort(params: {}): StorageCursor {
|
||||
return this
|
||||
}
|
||||
|
||||
toArray(): Promise<any[]> {
|
||||
return Promise.resolve(Array.from(this._map.values()))
|
||||
}
|
||||
|
||||
skip(offset: number): StorageCursor {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export class MapStorage extends Storage {
|
||||
private readonly _mapPool
|
||||
private readonly _mapPoolId
|
||||
|
||||
constructor(dsn: string) {
|
||||
super(dsn)
|
||||
this._mapPoolId = {}
|
||||
this._mapPool = {}
|
||||
}
|
||||
|
||||
async find(name: string, query: Object): Promise<StorageCursor> {
|
||||
if (!(name in this._mapPool)) {
|
||||
return new MapCursor(new Map())
|
||||
}
|
||||
const map: Map<any, any> = new Map()
|
||||
for (let [_, elem] of this._mapPool[name]) {
|
||||
nextParam: for (let [key, val] of Object.entries(query)) {
|
||||
if (key in elem) {
|
||||
for (let symbol of ['=', '>', '<', '!']) {
|
||||
if ((val + '').startsWith(symbol)) {
|
||||
if (eval(elem[key] + val + '')) {
|
||||
map.set(_, elem)
|
||||
break nextParam
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elem[key] == val) {
|
||||
map.set(_, elem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new MapCursor(map)
|
||||
}
|
||||
|
||||
count(name: string, query: object): Promise<number> {
|
||||
return Promise.resolve(0)
|
||||
}
|
||||
|
||||
remove(collectionName: string, uniqKeyName: string, uniqKey: string): Promise<boolean> {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
async save(name: string, idKey: string, data: Object): Promise<string> {
|
||||
if (!(name in this._mapPool)) {
|
||||
this._mapPool[name] = new Map()
|
||||
this._mapPoolId[name] = {}
|
||||
}
|
||||
|
||||
const id = data[idKey] + ''
|
||||
this._mapPool[name].set(id, data)
|
||||
if (!(id in this._mapPoolId[name])) {
|
||||
this._mapPoolId[name][id] = Date.now()
|
||||
}
|
||||
return Promise.resolve(this._mapPoolId[name][id])
|
||||
}
|
||||
|
||||
createSession(): StorageSession {
|
||||
return new MapStorageSession()
|
||||
}
|
||||
}
|
||||
|
||||
class MapStorageSession extends StorageSession {
|
||||
async commit(fn, options?: {}): Promise<any> {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
|
||||
async start(options?: {}): Promise<any> {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
describe('Storage', function () {
|
||||
describe('#save', function () {
|
||||
it('should return inner id', async () => {
|
||||
const storage = new MapStorage('')
|
||||
|
||||
await storage.save('items', 'id', { id: 100, value: 'one' })
|
||||
const _id2 = await storage.save('items', 'id', { id: 102, value: 'two' })
|
||||
|
||||
const _id3 = await storage.save('items', 'id', { id: 102, value: 'two' })
|
||||
assert.equal(_id2, _id3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#find', () => {
|
||||
it('should return cursor', async () => {
|
||||
const storage = new MapStorage('')
|
||||
await storage.save('items', 'id', { id: 1, value: 'one' })
|
||||
await storage.save('items', 'id', { id: 2, value: 'two' })
|
||||
await storage.save('elements', 'id', { id: 1, value: 2 })
|
||||
await storage.save('elements', 'id', { id: 2, value: 11 })
|
||||
await storage.save('elements', 'id', { id: 3, value: 12 })
|
||||
|
||||
let cursor
|
||||
cursor = await storage.find('items', { value: 'one' })
|
||||
assert.equal((await cursor.toArray()).length, 1)
|
||||
|
||||
cursor = await storage.find('elements', { value: '>10' })
|
||||
assert.equal((await cursor.toArray()).length, 2)
|
||||
})
|
||||
})
|
||||
})
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"target": "ES2020",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"allowJs": false,
|
||||
"checkJs": false,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node", "jest", "rfdc", "mongodb", "bson"],
|
||||
"watch": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"strict": false
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user