Тесты работают, TS билдится

This commit is contained in:
Nikita Dezzpil Orlov 2021-09-29 17:11:49 +03:00
parent e08d9757bb
commit 9f3a64d76f
36 changed files with 17901 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

8
.idea/.gitignore vendored Normal file
View 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
View 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>

View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View 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
View File

View File

0
spec/data.d.ts vendored Normal file
View File

1
spec/data.d.ts.map Normal file
View 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
View File

1
spec/entity.d.ts.map Normal file
View 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
View File

1
spec/repo.d.ts.map Normal file
View 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
View File

1
spec/storage.d.ts.map Normal file
View 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
View 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
View 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
View 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
View 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
View 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()
}
}

View 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;

View 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
View File

48
src/tests/data.test.ts Normal file
View 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)
})
})
})

View 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
View 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
View 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/**/*"
]
}