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

This commit is contained in:
Nikita Dezzpil Orlov committed 2021-09-29 17:11:49 +03:00
1 parent e08d9757bb
commit 9f3a64d76f
36 files changed
+17901

No files matched your search

+50
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
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
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
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
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()
}
}
+13
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;
+10
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());
//}
View File
Whitespace-only changes.
+48
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)
})
})
})
+50
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
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)
})
})
})