Тесты работают, 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