Compare commits
182 Commits
Author | SHA1 | Date | |
---|---|---|---|
7a08700451 | |||
ebaf3e685c | |||
c8d51a30d8 | |||
d957e911de | |||
fee936c75f | |||
ac867401de | |||
c066464526 | |||
0105aa2a18 | |||
4c2477c269 | |||
ea0d2bb251 | |||
b3e30a8711 | |||
64621dd38f | |||
117c257a27 | |||
b30522c505 | |||
57d2d56d00 | |||
90751002aa | |||
7606e074a5 | |||
7ec39e397e | |||
21d8d3dc32 | |||
6d456955d8 | |||
d08544c782 | |||
bda9ac8a07 | |||
d27dafba2b | |||
b6594de18c | |||
d9246cbeac | |||
9a5864656e | |||
307f0c7277 | |||
62dc897e73 | |||
552b344914 | |||
5a2cc2406c | |||
73a11370b6 | |||
162265f353 | |||
06776d74c8 | |||
b4cd6b0fe1 | |||
b282f69b35 | |||
203a284c88 | |||
30ae641a9c | |||
cfe733621f | |||
1f76e2478e | |||
7d668bee05 | |||
bef7f68360 | |||
56e9754725 | |||
30d81581cf | |||
5e9db12955 | |||
ad2f422c86 | |||
17ce14bcb9 | |||
32319e6e77 | |||
4cd284eaa9 | |||
00ec2e57c2 | |||
765356ce3d | |||
56b8581d2b | |||
37a9df9086 | |||
090fb668cd | |||
a1c807261c | |||
a2ccf15f69 | |||
84d48f1914 | |||
1e258e5ffb | |||
19d5f553b9 | |||
7a257ea925 | |||
2fa1e89f34 | |||
d6b3896dd3 | |||
49b11b17ce | |||
4ac8a4c0cd | |||
7f9983382a | |||
54f529b0a7 | |||
f542463bf6 | |||
1235ae2eb3 | |||
8166d2f7c2 | |||
7c9f27e02f | |||
842e4b280b | |||
009f3297b2 | |||
2ff3a4e0b7 | |||
0e55cd8876 | |||
eccdf3f00a | |||
c7544133d9 | |||
c7c9acf5bd | |||
c99ec50853 | |||
4dd9557e1d | |||
52b34a6da1 | |||
1bf74fe04d | |||
fdd875ad31 | |||
a7bf0c0298 | |||
59d6336e43 | |||
e0fc81179a | |||
5aa81a56a2 | |||
9ae26177b8 | |||
26ac52d6c5 | |||
fb39463b7d | |||
44acba80c1 | |||
8cf8315577 | |||
9b44b64a50 | |||
699e25201c | |||
2ef9aace68 | |||
cc55a57dfd | |||
b2df512552 | |||
23c62fbd69 | |||
5f70ea0b05 | |||
49a595876a | |||
db38a1ef85 | |||
94854638dd | |||
902fab4cc0 | |||
ed3b19abc5 | |||
5b88da7dce | |||
df273e9efa | |||
fd590e0be3 | |||
ef97b390d4 | |||
cd14eb8bf3 | |||
f48443dcd3 | |||
3f28ff80cb | |||
64005a0b32 | |||
8a77bb3281 | |||
25f50ecf51 | |||
ad87f8147b | |||
da0c9873eb | |||
2fcd3f1550 | |||
f726cf4c5b | |||
c198969fae | |||
be1badeb23 | |||
fe065b966f | |||
811e2490b8 | |||
206ccd40e9 | |||
055298172f | |||
278f3c8169 | |||
0709ba921b | |||
de1f1110b4 | |||
30f4254428 | |||
1c4b03e647 | |||
27cc7651ba | |||
355a2a3f2b | |||
a739582861 | |||
37f9a64735 | |||
83a5170591 | |||
f94363cf31 | |||
df02e5bb71 | |||
38e438c54f | |||
11bc1ac6dc | |||
3431e94ddd | |||
739e040776 | |||
28d57efd9e | |||
f50a61308c | |||
42aa9f9f8a | |||
3f591ff9d8 | |||
7b33347b4c | |||
3f11cbf595 | |||
cf1ec7f9eb | |||
54060deb8f | |||
48cffb5ac2 | |||
8301eb79a2 | |||
ad6366a294 | |||
cb6c03ebfe | |||
551916fe5c | |||
9b3892c1e8 | |||
c1b1af9c5d | |||
d3a3d5be9d | |||
c9a734d879 | |||
856e8e7d1f | |||
7a4d557724 | |||
7cbd0bd99b | |||
e10f6585a5 | |||
5c8dffdd9c | |||
846996eeac | |||
668df09ba0 | |||
03656f4ca0 | |||
c4c612f3a9 | |||
e357f7581c | |||
9697b1e48b | |||
aeb35705d4 | |||
236c8c6551 | |||
1f28db15e7 | |||
86d600e287 | |||
bb81530dac | |||
b9f9b36b87 | |||
df2fadfa01 | |||
8b2beb3485 | |||
144a620f43 | |||
c241247845 | |||
81e39d09e4 | |||
8e51b518b1 | |||
8308d8d03b | |||
97365ddf29 | |||
55d96fa68d | |||
54ec6accdf |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
name: Default (not tags)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags-ignore:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||||
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
|
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install pnpm and npmci
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
|
||||||
|
- name: Run npm prepare
|
||||||
|
run: npmci npm prepare
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --prod
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Audit development dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: security
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Test stable
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm test
|
||||||
|
|
||||||
|
- name: Test build
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm build
|
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
name: Default (tags)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||||
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||||
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
|
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --prod
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Audit development dependencies
|
||||||
|
run: |
|
||||||
|
npmci command npm config set registry https://registry.npmjs.org
|
||||||
|
npmci command pnpm audit --audit-level=high --dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: security
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Test stable
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm test
|
||||||
|
|
||||||
|
- name: Test build
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
npmci npm build
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm publish
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
needs: test
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ${{ env.IMAGE }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
pnpm install -g pnpm
|
||||||
|
pnpm install -g @shipzone/npmci
|
||||||
|
npmci npm prepare
|
||||||
|
|
||||||
|
- name: Code quality
|
||||||
|
run: |
|
||||||
|
npmci command npm install -g typescript
|
||||||
|
npmci npm install
|
||||||
|
|
||||||
|
- name: Trigger
|
||||||
|
run: npmci trigger
|
||||||
|
|
||||||
|
- name: Build docs and upload artifacts
|
||||||
|
run: |
|
||||||
|
npmci node install stable
|
||||||
|
npmci npm install
|
||||||
|
pnpm install -g @gitzone/tsdoc
|
||||||
|
npmci command tsdoc
|
||||||
|
continue-on-error: true
|
137
.gitlab-ci.yml
137
.gitlab-ci.yml
@ -1,137 +0,0 @@
|
|||||||
# gitzone ci_default
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- .npmci_cache/
|
|
||||||
key: '$CI_BUILD_STAGE'
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- security
|
|
||||||
- test
|
|
||||||
- release
|
|
||||||
- metadata
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# security stage
|
|
||||||
# ====================
|
|
||||||
mirror:
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci git mirror
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
auditProductionDependencies:
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci command npm install --production --ignore-scripts
|
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
- npmci command npm audit --audit-level=high --only=prod --production
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
auditDevDependencies:
|
|
||||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
|
||||||
stage: security
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci command npm install --ignore-scripts
|
|
||||||
- npmci command npm config set registry https://registry.npmjs.org
|
|
||||||
- npmci command npm audit --audit-level=high --only=dev
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# test stage
|
|
||||||
# ====================
|
|
||||||
|
|
||||||
testStable:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci npm test
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
testBuild:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command npm run build
|
|
||||||
coverage: /\d+.?\d+?\%\s*coverage/
|
|
||||||
tags:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
script:
|
|
||||||
- npmci node install stable
|
|
||||||
- npmci npm publish
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
# ====================
|
|
||||||
# metadata stage
|
|
||||||
# ====================
|
|
||||||
codequality:
|
|
||||||
stage: metadata
|
|
||||||
allow_failure: true
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
script:
|
|
||||||
- npmci command npm install -g tslint typescript
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- priv
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci trigger
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
|
|
||||||
pages:
|
|
||||||
stage: metadata
|
|
||||||
script:
|
|
||||||
- npmci node install lts
|
|
||||||
- npmci command npm install -g @gitzone/tsdoc
|
|
||||||
- npmci npm prepare
|
|
||||||
- npmci npm install
|
|
||||||
- npmci command tsdoc
|
|
||||||
tags:
|
|
||||||
- lossless
|
|
||||||
- docker
|
|
||||||
- notpriv
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
artifacts:
|
|
||||||
expire_in: 1 week
|
|
||||||
paths:
|
|
||||||
- public
|
|
||||||
allow_failure: true
|
|
24
.vscode/launch.json
vendored
24
.vscode/launch.json
vendored
@ -2,28 +2,10 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "current file",
|
"command": "npm test",
|
||||||
"type": "node",
|
"name": "Run npm test",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": [
|
"type": "node-terminal"
|
||||||
"${relativeFile}"
|
|
||||||
],
|
|
||||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
|
||||||
"cwd": "${workspaceRoot}",
|
|
||||||
"protocol": "inspector",
|
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "test.ts",
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"args": [
|
|
||||||
"test/test.ts"
|
|
||||||
],
|
|
||||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
|
||||||
"cwd": "${workspaceRoot}",
|
|
||||||
"protocol": "inspector",
|
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
179
changelog.md
Normal file
179
changelog.md
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-10 - 5.3.0 - feat(docs)
|
||||||
|
Enhance documentation with updated installation instructions and comprehensive usage examples covering advanced features such as deep queries, automatic indexing, and distributed coordination.
|
||||||
|
|
||||||
|
- Added pnpm installation command
|
||||||
|
- Updated User model example to include ObjectId, Binary, and custom serialization
|
||||||
|
- Expanded CRUD operations examples with cursor methods and deep query support
|
||||||
|
- Enhanced sections on EasyStore, real-time data watching with RxJS integration, and managed collections
|
||||||
|
- Included detailed examples for transactions, deep object queries, and document lifecycle hooks
|
||||||
|
|
||||||
|
## 2025-02-03 - 5.2.12 - fix(documentation)
|
||||||
|
Remove license badge from README
|
||||||
|
|
||||||
|
- Removed the license badge from the README file, ensuring compliance with branding guidelines.
|
||||||
|
|
||||||
|
## 2025-02-03 - 5.2.11 - fix(documentation)
|
||||||
|
Updated project documentation for accuracy and added advanced feature details
|
||||||
|
|
||||||
|
- Added details for EasyStore, Distributed Coordination, and Real-time Data Watching features.
|
||||||
|
- Updated database connection setup instructions to include user authentication.
|
||||||
|
- Re-organized advanced usage section to showcase additional features separately.
|
||||||
|
|
||||||
|
## 2024-09-05 - 5.2.10 - fix(smartdata.classes.doc)
|
||||||
|
Fix issue with array handling in convertFilterForMongoDb function
|
||||||
|
|
||||||
|
- Corrected the logic to properly handle array filters in the convertFilterForMongoDb function to avoid incorrect assignments.
|
||||||
|
|
||||||
|
## 2024-09-05 - 5.2.9 - fix(smartdata.classes.doc)
|
||||||
|
Fixed issue with convertFilterForMongoDb to handle array operators.
|
||||||
|
|
||||||
|
- Updated the convertFilterForMongoDb function in smartdata.classes.doc.ts to properly handle array operators like $in and $all.
|
||||||
|
|
||||||
|
## 2024-09-05 - 5.2.8 - fix(smartdata.classes.doc)
|
||||||
|
Fix key handling in convertFilterForMongoDb function
|
||||||
|
|
||||||
|
- Fixed an issue in convertFilterForMongoDb that allowed keys with dots which could cause errors.
|
||||||
|
|
||||||
|
## 2024-09-05 - 5.2.7 - fix(core)
|
||||||
|
Fixed issue with handling filter keys containing dots in smartdata.classes.doc.ts
|
||||||
|
|
||||||
|
- Fixed an error in the convertFilterForMongoDb function which previously threw an error when keys contained dots.
|
||||||
|
|
||||||
|
## 2024-06-18 - 5.2.6 - Chore
|
||||||
|
Maintenance Release
|
||||||
|
|
||||||
|
- Release version 5.2.6
|
||||||
|
|
||||||
|
## 2024-05-31 - 5.2.2 - Bug Fixes
|
||||||
|
Fixes and Maintenance
|
||||||
|
|
||||||
|
- Fixed issue where `_createdAt` and `_updatedAt` registered saveableProperties for all document types
|
||||||
|
|
||||||
|
## 2024-04-15 - 5.1.2 - New Feature
|
||||||
|
Enhancements and Bug Fixes
|
||||||
|
|
||||||
|
- Added static `.getCount({})` method to `SmartDataDbDoc`
|
||||||
|
- Changed fields `_createdAt` and `_updatedAt` to ISO format
|
||||||
|
|
||||||
|
## 2024-04-14 - 5.0.43 - New Feature
|
||||||
|
New Feature Addition
|
||||||
|
|
||||||
|
- Added default `_createdAt` and `_updatedAt` fields, fixes #1
|
||||||
|
|
||||||
|
## 2024-03-30 - 5.0.41 - Bug Fixes
|
||||||
|
Improvements and Fixes
|
||||||
|
|
||||||
|
- Improved `tsconfig.json` for ES Module use
|
||||||
|
|
||||||
|
## 2023-07-10 - 5.0.20 - Chore
|
||||||
|
Organizational Changes
|
||||||
|
|
||||||
|
- Switched to new org scheme
|
||||||
|
|
||||||
|
## 2023-07-21 - 5.0.21 to 5.0.26 - Fixes
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2023-07-21 - 5.0.20 - Chore
|
||||||
|
Organizational Changes
|
||||||
|
|
||||||
|
- Switch to the new org scheme
|
||||||
|
|
||||||
|
## 2023-06-25 - 5.0.14 to 5.0.19 - Fixes
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2022-05-17 - 5.0.0 - Major Update
|
||||||
|
Breaking Changes
|
||||||
|
|
||||||
|
- Switched to ESM
|
||||||
|
|
||||||
|
## 2022-05-18 - 5.0.2 - Bug Fixes
|
||||||
|
Bug Fixes
|
||||||
|
|
||||||
|
- The `watcher.changeSubject` now emits the correct type into observer functions
|
||||||
|
|
||||||
|
## 2022-05-17 - 5.0.1 - Chore
|
||||||
|
Testing Improvements
|
||||||
|
|
||||||
|
- Tests now use `@pushrocks/smartmongo` backed by `wiredTiger`
|
||||||
|
|
||||||
|
## 2022-05-17 to 2022-11-08 - 5.0.8 to 5.0.10
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2021-11-12 - 4.0.17 to 4.0.20
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2021-09-17 - 4.0.10 to 4.0.16
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2021-06-09 - 4.0.1 to 4.0.9
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2021-06-06 - 4.0.0 - Major Update
|
||||||
|
Major Release
|
||||||
|
|
||||||
|
- Maintenance and core updates
|
||||||
|
|
||||||
|
## 2021-05-17 - 3.1.56 - Chore
|
||||||
|
Maintenance Release
|
||||||
|
|
||||||
|
- Release version 3.1.56
|
||||||
|
|
||||||
|
## 2020-09-09 - 3.1.44 to 3.1.52
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2020-06-12 - 3.1.26 to 3.1.28
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2020-02-18 - 3.1.23 to 3.1.25
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2019-09-11 - 3.1.20 to 3.1.22
|
||||||
|
Multiple Fix Releases
|
||||||
|
|
||||||
|
- Various core updates and bug fixes
|
||||||
|
|
||||||
|
## 2018-07-10 - 3.0.5 - New Feature
|
||||||
|
Added Feature
|
||||||
|
|
||||||
|
- Added custom unique indexes to `SmartdataDoc`
|
||||||
|
|
||||||
|
## 2018-07-08 - 3.0.1 - Chore
|
||||||
|
Dependencies Update
|
||||||
|
|
||||||
|
- Updated mongodb dependencies
|
||||||
|
|
||||||
|
## 2018-07-08 - 3.0.0 - Major Update
|
||||||
|
Refactor and Cleanup
|
||||||
|
|
||||||
|
- Cleaned project structure
|
||||||
|
|
||||||
|
## 2018-01-16 - 2.0.7 - Breaking Change
|
||||||
|
Big Changes
|
||||||
|
|
||||||
|
- Switched to `@pushrocks` scope and moved from `rethinkdb` to `mongodb`
|
||||||
|
|
||||||
|
## 2018-01-12 - 2.0.0 - Major Release
|
||||||
|
Core Updates
|
||||||
|
|
||||||
|
- Updated CI configurations
|
||||||
|
|
@ -12,12 +12,25 @@
|
|||||||
"gitzone": {
|
"gitzone": {
|
||||||
"projectType": "npm",
|
"projectType": "npm",
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "gitlab.com",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "pushrocks",
|
"gitscope": "push.rocks",
|
||||||
"gitrepo": "smartdata",
|
"gitrepo": "smartdata",
|
||||||
"shortDescription": "do more with data",
|
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
|
||||||
"npmPackagename": "@pushrocks/smartdata",
|
"npmPackagename": "@push.rocks/smartdata",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"data manipulation",
|
||||||
|
"NoSQL",
|
||||||
|
"MongoDB",
|
||||||
|
"TypeScript",
|
||||||
|
"data validation",
|
||||||
|
"collections",
|
||||||
|
"custom data types",
|
||||||
|
"ODM"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tsdoc": {
|
||||||
|
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
26257
package-lock.json
generated
26257
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@ -1,49 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "@pushrocks/smartdata",
|
"name": "@push.rocks/smartdata",
|
||||||
"version": "4.0.1",
|
"version": "5.3.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "do more with data",
|
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "tstest test/",
|
||||||
"testLocal": "(npmdocker)",
|
"build": "tsbuild --web --allowimplicitany",
|
||||||
"build": "(tsbuild --web)"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+ssh://git@gitlab.com/pushrocks/smartdata.git"
|
"url": "https://code.foss.global/push.rocks/smartdata.git"
|
||||||
},
|
},
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://gitlab.com/pushrocks/smartdata/issues"
|
"url": "https://gitlab.com/pushrocks/smartdata/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/pushrocks/smartdata#README",
|
"homepage": "https://code.foss.global/push.rocks/smartdata",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pushrocks/lik": "^4.0.20",
|
"@push.rocks/lik": "^6.0.14",
|
||||||
"@pushrocks/smartlog": "^2.0.39",
|
"@push.rocks/smartdelay": "^3.0.1",
|
||||||
"@pushrocks/smartpromise": "^3.1.5",
|
"@push.rocks/smartlog": "^3.0.2",
|
||||||
"@pushrocks/smartstring": "^3.0.24",
|
"@push.rocks/smartmongo": "^2.0.10",
|
||||||
"@tsclass/tsclass": "^3.0.33",
|
"@push.rocks/smartpromise": "^4.0.2",
|
||||||
"@types/lodash": "^4.14.169",
|
"@push.rocks/smartrx": "^3.0.7",
|
||||||
"@types/mongodb": "^3.6.12",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"lodash": "^4.17.21",
|
"@push.rocks/smarttime": "^4.0.6",
|
||||||
"mongodb": "^3.6.6",
|
"@push.rocks/smartunique": "^3.0.8",
|
||||||
"runtime-type-checks": "0.0.4"
|
"@push.rocks/taskbuffer": "^3.1.7",
|
||||||
|
"@tsclass/tsclass": "^4.0.52",
|
||||||
|
"mongodb": "^6.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gitzone/tsbuild": "^2.1.25",
|
"@gitzone/tsbuild": "^2.1.66",
|
||||||
"@gitzone/tstest": "^1.0.54",
|
"@gitzone/tsrun": "^1.2.44",
|
||||||
"@pushrocks/qenv": "^4.0.10",
|
"@gitzone/tstest": "^1.0.77",
|
||||||
"@pushrocks/smartunique": "^3.0.3",
|
"@push.rocks/qenv": "^6.0.5",
|
||||||
"@pushrocks/tapbundle": "^3.2.14",
|
"@push.rocks/tapbundle": "^5.0.22",
|
||||||
"@types/mongodb-memory-server": "^2.3.0",
|
"@types/node": "^20.11.30"
|
||||||
"@types/node": "^15.3.0",
|
|
||||||
"@types/shortid": "0.0.29",
|
|
||||||
"mongodb-memory-server": "^6.9.6",
|
|
||||||
"tslint": "^6.1.3",
|
|
||||||
"tslint-config-prettier": "^1.18.0"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@ -59,5 +57,15 @@
|
|||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"data manipulation",
|
||||||
|
"NoSQL",
|
||||||
|
"MongoDB",
|
||||||
|
"TypeScript",
|
||||||
|
"data validation",
|
||||||
|
"collections",
|
||||||
|
"custom data types",
|
||||||
|
"ODM"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
6881
pnpm-lock.yaml
generated
Normal file
6881
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
readme.hints.md
Normal file
0
readme.hints.md
Normal file
577
readme.md
577
readme.md
@ -1,156 +1,493 @@
|
|||||||
# @pushrocks/smartdata
|
# @push.rocks/smartdata
|
||||||
do more with data
|
|
||||||
|
|
||||||
## Availabililty and Links
|
[](https://www.npmjs.com/package/@push.rocks/smartdata)
|
||||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/smartdata)
|
|
||||||
* [gitlab.com (source)](https://gitlab.com/pushrocks/smartdata)
|
|
||||||
* [github.com (source mirror)](https://github.com/pushrocks/smartdata)
|
|
||||||
* [docs (typedoc)](https://pushrocks.gitlab.io/smartdata/)
|
|
||||||
|
|
||||||
## Status for master
|
A powerful TypeScript-first MongoDB wrapper that provides advanced features for distributed systems, real-time data synchronization, and easy data management.
|
||||||
|
|
||||||
Status Category | Status Badge
|
## Features
|
||||||
-- | --
|
|
||||||
GitLab Pipelines | [](https://lossless.cloud)
|
- **Type-Safe MongoDB Integration**: Full TypeScript support with decorators for schema definition
|
||||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
- **Document Management**: Type-safe CRUD operations with automatic timestamp tracking
|
||||||
npm | [](https://lossless.cloud)
|
- **EasyStore**: Simple key-value storage with automatic persistence and sharing between instances
|
||||||
Snyk | [](https://lossless.cloud)
|
- **Distributed Coordination**: Built-in support for leader election and distributed task management
|
||||||
TypeScript Support | [](https://lossless.cloud)
|
- **Real-time Data Sync**: Watchers for real-time data changes with RxJS integration
|
||||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
- **Connection Management**: Automatic connection handling with connection pooling
|
||||||
Code Style | [](https://lossless.cloud)
|
- **Collection Management**: Type-safe collection operations with automatic indexing
|
||||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
- **Deep Query Type Safety**: Fully type-safe queries for nested object properties with `DeepQuery<T>`
|
||||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
- **MongoDB Compatibility**: Support for all MongoDB query operators and advanced features
|
||||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
- **Enhanced Cursors**: Chainable, type-safe cursor API with memory efficient data processing
|
||||||
Platform support | [](https://lossless.cloud) [](https://lossless.cloud)
|
- **Type Conversion**: Automatic handling of MongoDB types like ObjectId and Binary data
|
||||||
|
- **Serialization Hooks**: Custom serialization and deserialization of document properties
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js >= 16.x
|
||||||
|
- MongoDB >= 4.4
|
||||||
|
- TypeScript >= 4.x (for development)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
To install `@push.rocks/smartdata`, use npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @push.rocks/smartdata --save
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with pnpm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @push.rocks/smartdata
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
`@push.rocks/smartdata` enables efficient data handling and operation management with a focus on using MongoDB. It leverages TypeScript for strong typing and ESM syntax for modern JavaScript usage.
|
||||||
|
|
||||||
Use TypeScript for best in class instellisense.
|
### Setting Up and Connecting to the Database
|
||||||
|
Before interacting with the database, you need to set up and establish a connection. The `SmartdataDb` class handles connection pooling and automatic reconnection.
|
||||||
smartdata is an ODM that adheres to TypeScript practices and uses classes to organize data.
|
|
||||||
It uses RethinkDB as persistent storage.
|
|
||||||
|
|
||||||
## Intention
|
|
||||||
|
|
||||||
There are many ODMs out there, however when we searched for an ODM that uses TypeScript,
|
|
||||||
acts smart while still embracing the NoSQL idea we didn't find a matching solution.
|
|
||||||
This is why we started smartdata.
|
|
||||||
|
|
||||||
How RethinkDB's terms map to the ones of smartdata:
|
|
||||||
|
|
||||||
| MongoDb term | smartdata class |
|
|
||||||
| ------------ | ----------------------------- |
|
|
||||||
| Database | smartdata.SmartdataDb |
|
|
||||||
| Collection | smartdata.SmartdataCollection |
|
|
||||||
| Document | smartdata.SmartadataDoc |
|
|
||||||
|
|
||||||
### class Db
|
|
||||||
|
|
||||||
represents a Database. Naturally it has .connect() etc. methods on it.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Assuming toplevel await
|
import { SmartdataDb } from '@push.rocks/smartdata';
|
||||||
import * as smartdata from 'smartdata';
|
|
||||||
|
|
||||||
const smartdataDb = new smartdata.SmartdataDb({
|
// Create a new instance of SmartdataDb with MongoDB connection details
|
||||||
mongoDbUrl: '//someurl',
|
const db = new SmartdataDb({
|
||||||
mongoDbName: 'myDatabase',
|
mongoDbUrl: 'mongodb://<USERNAME>:<PASSWORD>@localhost:27017/<DBNAME>',
|
||||||
mongoDbPass: 'mypassword',
|
mongoDbName: 'your-database-name',
|
||||||
|
mongoDbUser: 'your-username',
|
||||||
|
mongoDbPass: 'your-password',
|
||||||
});
|
});
|
||||||
|
|
||||||
await smartdataDb.connect();
|
// Initialize and connect to the database
|
||||||
|
// This sets up a connection pool with max 100 connections
|
||||||
|
await db.init();
|
||||||
```
|
```
|
||||||
|
|
||||||
### class DbCollection
|
### Defining Data Models
|
||||||
|
Data models in `@push.rocks/smartdata` are classes that represent collections and documents in your MongoDB database. Use decorators such as `@Collection`, `@unI`, and `@svDb` to define your data models.
|
||||||
represents a collection of objects.
|
|
||||||
A collection is defined by the object class (that is extending smartdata.dbdoc) it respresents
|
|
||||||
|
|
||||||
So to get to get access to a specific collection you document
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Assuming toplevel await
|
import { SmartDataDbDoc, Collection, unI, svDb, oid, bin, index } from '@push.rocks/smartdata';
|
||||||
// continues from the block before...
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
@smartdata.Collection(smartdataDb)
|
@Collection(() => db) // Associate this model with the database instance
|
||||||
class MyObject extends smartdata.DbDoc<MyObject /* ,[an optional interface to implement] */> {
|
class User extends SmartDataDbDoc<User, User> {
|
||||||
// read the next block about DbDoc
|
@unI()
|
||||||
@smartdata.svDb()
|
public id: string = 'unique-user-id'; // Mark 'id' as a unique index
|
||||||
property1: string; // @smartdata.svDb() marks the property for db save
|
|
||||||
|
@svDb()
|
||||||
property2: number; // this one is not marked, so it won't be save upon calling this.save()
|
public username: string; // Mark 'username' to be saved in DB
|
||||||
|
|
||||||
constructor() {
|
@svDb()
|
||||||
super(); // the super call is important ;) But you probably know that.
|
@index() // Create a regular index for this field
|
||||||
|
public email: string; // Mark 'email' to be saved in DB
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
@oid() // Automatically handle as ObjectId type
|
||||||
|
public organizationId: ObjectId; // Will be automatically converted to/from ObjectId
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
@bin() // Automatically handle as Binary data
|
||||||
|
public profilePicture: Buffer; // Will be automatically converted to/from Binary
|
||||||
|
|
||||||
|
@svDb({
|
||||||
|
serialize: (data) => JSON.stringify(data), // Custom serialization
|
||||||
|
deserialize: (data) => JSON.parse(data) // Custom deserialization
|
||||||
|
})
|
||||||
|
public preferences: Record<string, any>;
|
||||||
|
|
||||||
|
constructor(username: string, email: string) {
|
||||||
|
super();
|
||||||
|
this.username = username;
|
||||||
|
this.email = email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// start to instantiate instances of classes from scratch or database
|
|
||||||
|
|
||||||
const localObject = new MyObject({
|
|
||||||
property1: 'hi',
|
|
||||||
property2: {
|
|
||||||
deep: 3
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await localObject.save(); // saves the object to the database
|
|
||||||
|
|
||||||
// start retrieving instances
|
|
||||||
|
|
||||||
// .getInstance is staticly inheritied, yet fully typed static function to get instances with fully typed filters
|
|
||||||
const myInstance = await MyObject.getInstance({
|
|
||||||
property1: 'hi',
|
|
||||||
property2: {
|
|
||||||
deep: {
|
|
||||||
$gt: 2
|
|
||||||
} as any
|
|
||||||
}
|
|
||||||
}); // outputs a new instance of MyObject with the values from db assigned
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### class DbDoc
|
### CRUD Operations
|
||||||
|
`@push.rocks/smartdata` simplifies CRUD operations with intuitive methods on model instances.
|
||||||
|
|
||||||
represents a individual document in a collection
|
#### Create
|
||||||
and thereby is ideally suited to extend the class you want to actually store.
|
```typescript
|
||||||
|
const newUser = new User('myUsername', 'myEmail@example.com');
|
||||||
|
await newUser.save(); // Save the new user to the database
|
||||||
|
```
|
||||||
|
|
||||||
### CRUD operations
|
#### Read
|
||||||
|
```typescript
|
||||||
|
// Fetch a single user by a unique attribute
|
||||||
|
const user = await User.getInstance({ username: 'myUsername' });
|
||||||
|
|
||||||
smartdata supports full CRUD operations
|
// Fetch multiple users that match criteria
|
||||||
|
const users = await User.getInstances({ email: 'myEmail@example.com' });
|
||||||
|
|
||||||
**Store** or **Update** instances of classes to MongoDB:
|
// Using a cursor for large collections
|
||||||
DbDoc extends your class with the following methods:
|
const cursor = await User.getCursor({ active: true });
|
||||||
|
|
||||||
- async `.save()` will save (or update) the object you call it on only. Any referenced non-savable objects will not get stored.
|
// Process documents one at a time (memory efficient)
|
||||||
- async `.saveDeep()` does the same like `.save()`.
|
await cursor.forEach(async (user, index) => {
|
||||||
In addition it will look for properties that reference an object
|
// Process each user with its position
|
||||||
that extends DbDoc as well and call .saveDeep() on them as well.
|
console.log(`Processing user ${index}: ${user.username}`);
|
||||||
Loops are prevented
|
});
|
||||||
|
|
||||||
**Get** a new class instance from MongoDB:
|
// Chain cursor methods like in the MongoDB native driver
|
||||||
DbDoc exposes a static method that allows you specify a filter to retrieve a cloned class of the one you used to that doc at some point later in time:
|
const paginatedCursor = await User.getCursor({ active: true })
|
||||||
|
.limit(10) // Limit results
|
||||||
|
.skip(20) // Skip first 20 results
|
||||||
|
.sort({ createdAt: -1 }); // Sort by creation date descending
|
||||||
|
|
||||||
- static async `.getInstance({ /* filter props here */ })` gets you an instance that has the data of the first matched document as properties.
|
// Convert cursor to array (when you know the result set is small)
|
||||||
- static async `getInstances({ /* filter props here */ })` get you an array instances (one instance for every matched document).
|
const userArray = await paginatedCursor.toArray();
|
||||||
|
|
||||||
**Delete** instances from MongoDb:
|
// Other cursor operations
|
||||||
smartdata extends your class with a method to easily delete the doucment from DB:
|
const nextUser = await cursor.next(); // Get the next document
|
||||||
|
const hasMoreUsers = await cursor.hasNext(); // Check if more documents exist
|
||||||
|
const count = await cursor.count(); // Get the count of documents in the cursor
|
||||||
|
|
||||||
- async `.delete()`will delete the document from DB.
|
// Always close cursors when done with them
|
||||||
|
await cursor.close();
|
||||||
|
```
|
||||||
|
|
||||||
## TypeScript
|
#### Update
|
||||||
|
```typescript
|
||||||
|
// Assuming 'user' is an instance of User
|
||||||
|
user.email = 'newEmail@example.com';
|
||||||
|
await user.save(); // Update the user in the database
|
||||||
|
|
||||||
How does TypeScript play into this?
|
// Upsert operations (insert if not exists, update if exists)
|
||||||
Since you define your classes in TypeScript and types flow through smartdata in a generic way
|
const upsertedUser = await User.upsert(
|
||||||
you should get all the Intellisense and type checking you love when using smartdata.
|
{ id: 'user-123' }, // Query to find the user
|
||||||
smartdata itself also bundles typings. You don't need to install any additional types for smartdata.
|
{ // Fields to update or insert
|
||||||
|
username: 'newUsername',
|
||||||
|
email: 'newEmail@example.com'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
## Contribution
|
#### Delete
|
||||||
|
```typescript
|
||||||
|
// Assuming 'user' is an instance of User
|
||||||
|
await user.delete(); // Delete the user from the database
|
||||||
|
```
|
||||||
|
|
||||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
## Advanced Features
|
||||||
|
|
||||||
For further information read the linked docs at the top of this readme.
|
### EasyStore
|
||||||
|
EasyStore provides a simple key-value storage system with automatic persistence:
|
||||||
|
|
||||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
```typescript
|
||||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
// Create an EasyStore instance with a specific type
|
||||||
|
interface ConfigStore {
|
||||||
|
apiKey: string;
|
||||||
|
settings: {
|
||||||
|
theme: string;
|
||||||
|
notifications: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
[](https://maintainedby.lossless.com)
|
// Create a type-safe EasyStore
|
||||||
|
const store = await db.createEasyStore<ConfigStore>('app-config');
|
||||||
|
|
||||||
|
// Write and read data with full type safety
|
||||||
|
await store.writeKey('apiKey', 'secret-api-key-123');
|
||||||
|
await store.writeKey('settings', { theme: 'dark', notifications: true });
|
||||||
|
|
||||||
|
const apiKey = await store.readKey('apiKey'); // Type: string
|
||||||
|
const settings = await store.readKey('settings'); // Type: { theme: string, notifications: boolean }
|
||||||
|
|
||||||
|
// Check if a key exists
|
||||||
|
const hasKey = await store.hasKey('apiKey'); // true
|
||||||
|
|
||||||
|
// Delete a key
|
||||||
|
await store.deleteKey('apiKey');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Distributed Coordination
|
||||||
|
Built-in support for distributed systems with leader election:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create a distributed coordinator
|
||||||
|
const coordinator = new SmartdataDistributedCoordinator(db);
|
||||||
|
|
||||||
|
// Start coordination
|
||||||
|
await coordinator.start();
|
||||||
|
|
||||||
|
// Handle leadership changes
|
||||||
|
coordinator.on('leadershipChange', (isLeader) => {
|
||||||
|
if (isLeader) {
|
||||||
|
// This instance is now the leader
|
||||||
|
// Run leader-specific tasks
|
||||||
|
startPeriodicJobs();
|
||||||
|
} else {
|
||||||
|
// This instance is no longer the leader
|
||||||
|
stopPeriodicJobs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Access leadership status anytime
|
||||||
|
if (coordinator.isLeader) {
|
||||||
|
// Run leader-only operations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a task only on the leader
|
||||||
|
await coordinator.executeIfLeader(async () => {
|
||||||
|
// This code only runs on the leader instance
|
||||||
|
await runImportantTask();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop coordination when shutting down
|
||||||
|
await coordinator.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-time Data Watching
|
||||||
|
Watch for changes in your collections with RxJS integration using MongoDB Change Streams:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create a watcher for a specific collection with a query filter
|
||||||
|
const watcher = await User.watch({
|
||||||
|
active: true // Only watch for changes to active users
|
||||||
|
}, {
|
||||||
|
fullDocument: true, // Include the full document in change notifications
|
||||||
|
bufferTimeMs: 100 // Buffer changes for 100ms to reduce notification frequency
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to changes using RxJS
|
||||||
|
watcher.changeSubject.subscribe((change) => {
|
||||||
|
console.log('Change operation:', change.operationType); // 'insert', 'update', 'delete', etc.
|
||||||
|
console.log('Document changed:', change.docInstance); // The full document instance
|
||||||
|
|
||||||
|
// Handle different types of changes
|
||||||
|
if (change.operationType === 'insert') {
|
||||||
|
console.log('New user created:', change.docInstance.username);
|
||||||
|
} else if (change.operationType === 'update') {
|
||||||
|
console.log('User updated:', change.docInstance.username);
|
||||||
|
} else if (change.operationType === 'delete') {
|
||||||
|
console.log('User deleted');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual observation with event emitter pattern
|
||||||
|
watcher.on('change', (change) => {
|
||||||
|
console.log('Document changed:', change);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop watching when no longer needed
|
||||||
|
await watcher.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managed Collections
|
||||||
|
For more complex data models that require additional context:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Collection(() => db)
|
||||||
|
class ManagedDoc extends SmartDataDbDoc<ManagedDoc, ManagedDoc> {
|
||||||
|
@unI()
|
||||||
|
public id: string = 'unique-id';
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public data: string;
|
||||||
|
|
||||||
|
@managed()
|
||||||
|
public manager: YourCustomManager;
|
||||||
|
|
||||||
|
// The manager can provide additional functionality
|
||||||
|
async specialOperation() {
|
||||||
|
return this.manager.doSomethingSpecial(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Indexing
|
||||||
|
Define indexes directly in your model class:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Collection(() => db)
|
||||||
|
class Product extends SmartDataDbDoc<Product, Product> {
|
||||||
|
@unI() // Unique index
|
||||||
|
public id: string = 'product-id';
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
@index() // Regular index for faster queries
|
||||||
|
public category: string;
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
@index({ sparse: true }) // Sparse index with options
|
||||||
|
public optionalField?: string;
|
||||||
|
|
||||||
|
// Compound indexes can be defined in the collection decorator
|
||||||
|
@Collection(() => db, {
|
||||||
|
indexMap: {
|
||||||
|
compoundIndex: {
|
||||||
|
fields: { category: 1, name: 1 },
|
||||||
|
options: { background: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Support
|
||||||
|
Use MongoDB transactions for atomic operations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const session = await db.startSession();
|
||||||
|
try {
|
||||||
|
await session.withTransaction(async () => {
|
||||||
|
const user = await User.getInstance({ id: 'user-id' }, { session });
|
||||||
|
user.balance -= 100;
|
||||||
|
await user.save({ session });
|
||||||
|
|
||||||
|
const recipient = await User.getInstance({ id: 'recipient-id' }, { session });
|
||||||
|
recipient.balance += 100;
|
||||||
|
await user.save({ session });
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await session.endSession();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deep Object Queries
|
||||||
|
SmartData provides fully type-safe deep property queries with the `DeepQuery` type:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// If your document has nested objects
|
||||||
|
class UserProfile extends SmartDataDbDoc<UserProfile, UserProfile> {
|
||||||
|
@unI()
|
||||||
|
public id: string = 'profile-id';
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public user: {
|
||||||
|
details: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
address: {
|
||||||
|
city: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe string literals for dot notation
|
||||||
|
const usersInUSA = await UserProfile.getInstances({
|
||||||
|
'user.details.address.country': 'USA'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fully typed deep queries with the DeepQuery type
|
||||||
|
import { DeepQuery } from '@push.rocks/smartdata';
|
||||||
|
|
||||||
|
const typedQuery: DeepQuery<UserProfile> = {
|
||||||
|
id: 'profile-id',
|
||||||
|
'user.details.firstName': 'John',
|
||||||
|
'user.details.address.country': 'USA'
|
||||||
|
};
|
||||||
|
|
||||||
|
// TypeScript will error if paths are incorrect
|
||||||
|
const results = await UserProfile.getInstances(typedQuery);
|
||||||
|
|
||||||
|
// MongoDB query operators are supported
|
||||||
|
const operatorQuery: DeepQuery<UserProfile> = {
|
||||||
|
'user.details.address.country': 'USA',
|
||||||
|
'user.details.address.city': { $in: ['New York', 'Los Angeles'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredResults = await UserProfile.getInstances(operatorQuery);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Document Lifecycle Hooks
|
||||||
|
Implement custom logic at different stages of a document's lifecycle:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Collection(() => db)
|
||||||
|
class Order extends SmartDataDbDoc<Order, Order> {
|
||||||
|
@unI()
|
||||||
|
public id: string = 'order-id';
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public total: number;
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public items: string[];
|
||||||
|
|
||||||
|
// Called before saving the document
|
||||||
|
async beforeSave() {
|
||||||
|
// Calculate total based on items
|
||||||
|
this.total = await calculateTotal(this.items);
|
||||||
|
|
||||||
|
// Validate the document
|
||||||
|
if (this.items.length === 0) {
|
||||||
|
throw new Error('Order must have at least one item');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called after the document is saved
|
||||||
|
async afterSave() {
|
||||||
|
// Notify other systems about the saved order
|
||||||
|
await notifyExternalSystems(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called before deleting the document
|
||||||
|
async beforeDelete() {
|
||||||
|
// Check if order can be deleted
|
||||||
|
const canDelete = await checkOrderDeletable(this.id);
|
||||||
|
if (!canDelete) {
|
||||||
|
throw new Error('Order cannot be deleted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Connection Management
|
||||||
|
- Always call `db.init()` before using any database features
|
||||||
|
- Use `db.disconnect()` when shutting down your application
|
||||||
|
- Set appropriate connection pool sizes based on your application's needs
|
||||||
|
|
||||||
|
### Document Design
|
||||||
|
- Use appropriate decorators (`@svDb`, `@unI`, `@index`) to optimize database operations
|
||||||
|
- Implement type-safe models by properly extending `SmartDataDbDoc`
|
||||||
|
- Consider using interfaces to define document structures separately from implementation
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
- Use cursors for large datasets instead of loading all documents into memory
|
||||||
|
- Create appropriate indexes for frequent query patterns
|
||||||
|
- Use projections to limit the fields returned when you don't need the entire document
|
||||||
|
|
||||||
|
### Distributed Systems
|
||||||
|
- Implement proper error handling for leader election events
|
||||||
|
- Ensure all instances have synchronized clocks when using time-based coordination
|
||||||
|
- Use the distributed coordinator's task management features for coordinated operations
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
- Take advantage of the `DeepQuery<T>` type for fully type-safe queries
|
||||||
|
- Define proper types for your document models to enhance IDE auto-completion
|
||||||
|
- Use generic type parameters to specify exact document types when working with collections
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions to @push.rocks/smartdata! Here's how you can help:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
Please make sure to update tests as appropriate and follow our coding standards.
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
112
test/test.distributedcoordinator.ts
Normal file
112
test/test.distributedcoordinator.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
import * as smartmongo from '@push.rocks/smartmongo';
|
||||||
|
import type * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
|
import * as smartdata from '../ts/index.js';
|
||||||
|
import { SmartdataDistributedCoordinator, DistributedClass } from '../ts/smartdata.classes.distributedcoordinator.js'; // path might need adjusting
|
||||||
|
const totalInstances = 10;
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Connecting to the database server
|
||||||
|
// =======================================
|
||||||
|
|
||||||
|
let smartmongoInstance: smartmongo.SmartMongo;
|
||||||
|
let testDb: smartdata.SmartdataDb;
|
||||||
|
|
||||||
|
tap.test('should create a testinstance as database', async () => {
|
||||||
|
smartmongoInstance = await smartmongo.SmartMongo.createAndStart();
|
||||||
|
testDb = new smartdata.SmartdataDb(await smartmongoInstance.getMongoDescriptor());
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should instantiate DistributedClass', async (tools) => {
|
||||||
|
const instance = new DistributedClass();
|
||||||
|
expect(instance).toBeInstanceOf(DistributedClass);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DistributedClass should update the time', async (tools) => {
|
||||||
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
|
await distributedCoordinator.start();
|
||||||
|
const initialTime = distributedCoordinator.ownInstance.data.lastUpdated;
|
||||||
|
await distributedCoordinator.sendHeartbeat();
|
||||||
|
const updatedTime = distributedCoordinator.ownInstance.data.lastUpdated;
|
||||||
|
expect(updatedTime).toBeGreaterThan(initialTime);
|
||||||
|
await distributedCoordinator.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should instantiate SmartdataDistributedCoordinator', async (tools) => {
|
||||||
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
|
await distributedCoordinator.start();
|
||||||
|
expect(distributedCoordinator).toBeInstanceOf(SmartdataDistributedCoordinator);
|
||||||
|
await distributedCoordinator.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartdataDistributedCoordinator should update leader status', async (tools) => {
|
||||||
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
|
await distributedCoordinator.start();
|
||||||
|
await distributedCoordinator.checkAndMaybeLead();
|
||||||
|
expect(distributedCoordinator.ownInstance.data.elected).toBeOneOf([true, false]);
|
||||||
|
await distributedCoordinator.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartdataDistributedCoordinator should handle distributed task requests', async (tools) => {
|
||||||
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
|
await distributedCoordinator.start();
|
||||||
|
|
||||||
|
const mockTaskRequest: taskbuffer.distributedCoordination.IDistributedTaskRequest = {
|
||||||
|
submitterId: "mockSubmitter12345", // Some unique mock submitter ID
|
||||||
|
requestResponseId: 'uni879873462hjhfkjhsdf', // Some unique ID for the request-response
|
||||||
|
taskName: "SampleTask",
|
||||||
|
taskVersion: "1.0.0", // Assuming it's a version string
|
||||||
|
taskExecutionTime: Date.now(),
|
||||||
|
taskExecutionTimeout: 60000, // Let's say the timeout is 1 minute (60000 ms)
|
||||||
|
taskExecutionParallel: 5, // Let's assume max 5 parallel executions
|
||||||
|
status: 'requesting'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await distributedCoordinator.fireDistributedTaskRequest(mockTaskRequest);
|
||||||
|
console.log(response) // based on your expected structure for the response
|
||||||
|
await distributedCoordinator.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('SmartdataDistributedCoordinator should update distributed task requests', async (tools) => {
|
||||||
|
const distributedCoordinator = new SmartdataDistributedCoordinator(testDb);
|
||||||
|
|
||||||
|
await distributedCoordinator.start();
|
||||||
|
|
||||||
|
const mockTaskRequest: taskbuffer.distributedCoordination.IDistributedTaskRequest = {
|
||||||
|
submitterId: "mockSubmitter12345", // Some unique mock submitter ID
|
||||||
|
requestResponseId: 'uni879873462hjhfkjhsdf', // Some unique ID for the request-response
|
||||||
|
taskName: "SampleTask",
|
||||||
|
taskVersion: "1.0.0", // Assuming it's a version string
|
||||||
|
taskExecutionTime: Date.now(),
|
||||||
|
taskExecutionTimeout: 60000, // Let's say the timeout is 1 minute (60000 ms)
|
||||||
|
taskExecutionParallel: 5, // Let's assume max 5 parallel executions
|
||||||
|
status: 'requesting'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
await distributedCoordinator.updateDistributedTaskRequest(mockTaskRequest);
|
||||||
|
// Here, we can potentially check if a DB entry got updated or some other side-effect of the update method.
|
||||||
|
await distributedCoordinator.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should elect only one leader amongst multiple instances', async (tools) => {
|
||||||
|
const coordinators = Array.from({ length: totalInstances }).map(() => new SmartdataDistributedCoordinator(testDb));
|
||||||
|
await Promise.all(coordinators.map(coordinator => coordinator.start()));
|
||||||
|
const leaders = coordinators.filter(coordinator => coordinator.ownInstance.data.elected);
|
||||||
|
for (const leader of leaders) {
|
||||||
|
console.log(leader.ownInstance);
|
||||||
|
}
|
||||||
|
expect(leaders.length).toEqual(1);
|
||||||
|
|
||||||
|
// stopping clears a coordinator from being elected.
|
||||||
|
await Promise.all(coordinators.map(coordinator => coordinator.stop()));
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should clean up', async () => {
|
||||||
|
await smartmongoInstance.stopAndDumpToDir(`.nogit/dbdump/test.distributedcoordinator.ts`);
|
||||||
|
setTimeout(() => process.exit(), 2000);
|
||||||
|
})
|
||||||
|
|
||||||
|
tap.start({ throwOnError: true });
|
55
test/test.easystore.ts
Normal file
55
test/test.easystore.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as smartmongo from '@push.rocks/smartmongo';
|
||||||
|
import { smartunique } from '../ts/smartdata.plugins.js';
|
||||||
|
|
||||||
|
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
|
||||||
|
|
||||||
|
console.log(process.memoryUsage());
|
||||||
|
|
||||||
|
// the tested module
|
||||||
|
import * as smartdata from '../ts/index.js';
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Connecting to the database server
|
||||||
|
// =======================================
|
||||||
|
|
||||||
|
let smartmongoInstance: smartmongo.SmartMongo;
|
||||||
|
let testDb: smartdata.SmartdataDb;
|
||||||
|
|
||||||
|
tap.test('should create a testinstance as database', async () => {
|
||||||
|
smartmongoInstance = await smartmongo.SmartMongo.createAndStart();
|
||||||
|
testDb = new smartdata.SmartdataDb(await smartmongoInstance.getMongoDescriptor());
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.skip.test('should connect to atlas', async (tools) => {
|
||||||
|
const databaseName = `test-smartdata-${smartunique.shortId()}`;
|
||||||
|
testDb = new smartdata.SmartdataDb({
|
||||||
|
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGO_URL'),
|
||||||
|
mongoDbName: databaseName,
|
||||||
|
});
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
let easyStore: smartdata.EasyStore<{
|
||||||
|
key1: string;
|
||||||
|
key2: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
tap.test('should create an easystore', async () => {
|
||||||
|
easyStore = await testDb.createEasyStore('hellothere');
|
||||||
|
await easyStore.writeKey('key1', 'hello');
|
||||||
|
const retrievedKey = await easyStore.readKey('key1');
|
||||||
|
expect(retrievedKey).toEqual('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('close', async () => {
|
||||||
|
await testDb.mongoDb.dropDatabase();
|
||||||
|
await testDb.close();
|
||||||
|
if (smartmongoInstance) {
|
||||||
|
await smartmongoInstance.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start();
|
110
test/test.ts
110
test/test.ts
@ -1,51 +1,38 @@
|
|||||||
import { tap, expect } from '@pushrocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import { Qenv } from '@pushrocks/qenv';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as smartmongo from '@push.rocks/smartmongo';
|
||||||
|
import { smartunique } from '../ts/smartdata.plugins.js';
|
||||||
|
|
||||||
|
import * as mongodb from 'mongodb';
|
||||||
|
|
||||||
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
|
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
|
||||||
|
|
||||||
console.log(process.memoryUsage());
|
console.log(process.memoryUsage());
|
||||||
|
|
||||||
// the tested module
|
// the tested module
|
||||||
import * as smartdata from '../ts/index';
|
import * as smartdata from '../ts/index.js';
|
||||||
|
|
||||||
import * as mongoPlugin from 'mongodb-memory-server';
|
|
||||||
import { smartunique } from '../ts/smartdata.plugins';
|
|
||||||
|
|
||||||
// =======================================
|
// =======================================
|
||||||
// Connecting to the database server
|
// Connecting to the database server
|
||||||
// =======================================
|
// =======================================
|
||||||
|
|
||||||
|
let smartmongoInstance: smartmongo.SmartMongo;
|
||||||
let testDb: smartdata.SmartdataDb;
|
let testDb: smartdata.SmartdataDb;
|
||||||
let smartdataOptions: smartdata.IMongoDescriptor;
|
|
||||||
let mongod: mongoPlugin.MongoMemoryServer;
|
|
||||||
|
|
||||||
const totalCars = 2000;
|
const totalCars = 2000;
|
||||||
|
|
||||||
tap.skip.test('should create a testinstance as database', async () => {
|
tap.test('should create a testinstance as database', async () => {
|
||||||
mongod = new mongoPlugin.MongoMemoryServer({});
|
smartmongoInstance = await smartmongo.SmartMongo.createAndStart();
|
||||||
console.log('created mongod instance');
|
testDb = new smartdata.SmartdataDb(await smartmongoInstance.getMongoDescriptor());
|
||||||
await mongod._startUpInstance().catch((err) => {
|
await testDb.init();
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
console.log('mongod started');
|
|
||||||
smartdataOptions = {
|
|
||||||
mongoDbName: await mongod.getDbName(),
|
|
||||||
mongoDbPass: '',
|
|
||||||
mongoDbUrl: await mongod.getUri(),
|
|
||||||
};
|
|
||||||
console.log(smartdataOptions);
|
|
||||||
testDb = new smartdata.SmartdataDb(smartdataOptions);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should connect to atlas', async (tools) => {
|
tap.skip.test('should connect to atlas', async (tools) => {
|
||||||
const databaseName = `test-smartdata-${smartunique.shortId()}`;
|
const databaseName = `test-smartdata-${smartunique.shortId()}`;
|
||||||
testDb = new smartdata.SmartdataDb({
|
testDb = new smartdata.SmartdataDb({
|
||||||
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGO_URL'),
|
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGO_URL'),
|
||||||
mongoDbName: databaseName,
|
mongoDbName: databaseName,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should establish a connection to mongod', async () => {
|
|
||||||
await testDb.init();
|
await testDb.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -70,6 +57,9 @@ class Car extends smartdata.SmartDataDbDoc<Car, Car> {
|
|||||||
@smartdata.svDb()
|
@smartdata.svDb()
|
||||||
public brand: string;
|
public brand: string;
|
||||||
|
|
||||||
|
@smartdata.svDb()
|
||||||
|
public testBuffer = Buffer.from('hello');
|
||||||
|
|
||||||
@smartdata.svDb()
|
@smartdata.svDb()
|
||||||
deepData = {
|
deepData = {
|
||||||
sodeep: 'yes',
|
sodeep: 'yes',
|
||||||
@ -82,7 +72,12 @@ class Car extends smartdata.SmartDataDbDoc<Car, Car> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tap.test('should save the car to the db', async () => {
|
tap.test('should create a new id', async () => {
|
||||||
|
const newid = await Car.getNewId();
|
||||||
|
console.log(newid);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should save the car to the db', async (toolsArg) => {
|
||||||
const myCar = new Car('red', 'Volvo');
|
const myCar = new Car('red', 'Volvo');
|
||||||
await myCar.save();
|
await myCar.save();
|
||||||
|
|
||||||
@ -90,6 +85,9 @@ tap.test('should save the car to the db', async () => {
|
|||||||
await myCar2.save();
|
await myCar2.save();
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
|
const gottenCarInstance = await Car.getInstance({});
|
||||||
|
console.log(gottenCarInstance.testBuffer instanceof mongodb.Binary);
|
||||||
process.memoryUsage();
|
process.memoryUsage();
|
||||||
do {
|
do {
|
||||||
const myCar3 = new Car('red', 'Renault');
|
const myCar3 = new Car('red', 'Renault');
|
||||||
@ -107,7 +105,7 @@ tap.test('should save the car to the db', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('expect to get instance of Car with shallow match', async () => {
|
tap.test('expect to get instance of Car with shallow match', async () => {
|
||||||
const totalQueryCycles = totalCars / 4;
|
const totalQueryCycles = totalCars / 2;
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
do {
|
do {
|
||||||
const timeStart = Date.now();
|
const timeStart = Date.now();
|
||||||
@ -121,20 +119,20 @@ tap.test('expect to get instance of Car with shallow match', async () => {
|
|||||||
}ms to query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`
|
}ms to query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(myCars[0].deepData.sodeep).to.equal('yes');
|
expect(myCars[0].deepData.sodeep).toEqual('yes');
|
||||||
expect(myCars[0].brand).to.equal('Renault');
|
expect(myCars[0].brand).toEqual('Renault');
|
||||||
counter++;
|
counter++;
|
||||||
} while (counter < totalQueryCycles);
|
} while (counter < totalQueryCycles);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('expect to get instance of Car with deep match', async () => {
|
tap.test('expect to get instance of Car with deep match', async () => {
|
||||||
const totalQueryCycles = totalCars / 4;
|
const totalQueryCycles = totalCars / 6;
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
do {
|
do {
|
||||||
const timeStart = Date.now();
|
const timeStart = Date.now();
|
||||||
const myCars2 = await Car.getInstances({
|
const myCars2 = await Car.getInstances({
|
||||||
deepData: {
|
deepData: {
|
||||||
sodeep: 'yes'
|
sodeep: 'yes',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (counter % 10 === 0) {
|
if (counter % 10 === 0) {
|
||||||
@ -144,8 +142,8 @@ tap.test('expect to get instance of Car with deep match', async () => {
|
|||||||
}ms to deep query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`
|
}ms to deep query a set of 2000 with memory footprint ${process.memoryUsage().rss / 1e6} MB`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(myCars2[0].deepData.sodeep).to.equal('yes');
|
expect(myCars2[0].deepData.sodeep).toEqual('yes');
|
||||||
expect(myCars2[0].brand).to.equal('Volvo');
|
expect(myCars2[0].brand).toEqual('Volvo');
|
||||||
counter++;
|
counter++;
|
||||||
} while (counter < totalQueryCycles);
|
} while (counter < totalQueryCycles);
|
||||||
});
|
});
|
||||||
@ -154,7 +152,7 @@ tap.test('expect to get instance of Car and update it', async () => {
|
|||||||
const myCar = await Car.getInstance<Car>({
|
const myCar = await Car.getInstance<Car>({
|
||||||
brand: 'Volvo',
|
brand: 'Volvo',
|
||||||
});
|
});
|
||||||
expect(myCar.color).to.equal('red');
|
expect(myCar.color).toEqual('red');
|
||||||
myCar.color = 'blue';
|
myCar.color = 'blue';
|
||||||
await myCar.save();
|
await myCar.save();
|
||||||
});
|
});
|
||||||
@ -165,7 +163,7 @@ tap.test('should be able to delete an instance of car', async () => {
|
|||||||
color: 'blue',
|
color: 'blue',
|
||||||
});
|
});
|
||||||
console.log(myCars);
|
console.log(myCars);
|
||||||
expect(myCars[0].color).to.equal('blue');
|
expect(myCars[0].color).toEqual('blue');
|
||||||
for (const myCar of myCars) {
|
for (const myCar of myCars) {
|
||||||
await myCar.delete();
|
await myCar.delete();
|
||||||
}
|
}
|
||||||
@ -173,7 +171,7 @@ tap.test('should be able to delete an instance of car', async () => {
|
|||||||
const myCar2 = await Car.getInstance<Car>({
|
const myCar2 = await Car.getInstance<Car>({
|
||||||
brand: 'Volvo',
|
brand: 'Volvo',
|
||||||
});
|
});
|
||||||
expect(myCar2.color).to.equal('red');
|
expect(myCar2.color).toEqual('red');
|
||||||
});
|
});
|
||||||
|
|
||||||
// tslint:disable-next-line: max-classes-per-file
|
// tslint:disable-next-line: max-classes-per-file
|
||||||
@ -200,23 +198,39 @@ class Truck extends smartdata.SmartDataDbDoc<Car, Car> {
|
|||||||
tap.test('should store a new Truck', async () => {
|
tap.test('should store a new Truck', async () => {
|
||||||
const truck = new Truck('blue', 'MAN');
|
const truck = new Truck('blue', 'MAN');
|
||||||
await truck.save();
|
await truck.save();
|
||||||
const myTruck = await Truck.getInstance({ color: 'blue' });
|
|
||||||
myTruck.id = 'foo';
|
|
||||||
await myTruck.save();
|
|
||||||
const myTruck2 = await Truck.getInstance({ color: 'blue' });
|
const myTruck2 = await Truck.getInstance({ color: 'blue' });
|
||||||
console.log(myTruck2);
|
expect(myTruck2.color).toEqual('blue');
|
||||||
|
myTruck2.color = 'red';
|
||||||
|
await myTruck2.save();
|
||||||
|
const myTruck3 = await Truck.getInstance({ color: 'blue' });
|
||||||
|
expect(myTruck3).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should ', async () => {})
|
tap.test('should return a count', async () => {
|
||||||
|
const truckCount = await Truck.getCount();
|
||||||
|
expect(truckCount).toEqual(1);
|
||||||
|
})
|
||||||
|
|
||||||
|
tap.test('should use a cursor', async () => {
|
||||||
|
const cursor = await Car.getCursor({});
|
||||||
|
let counter = 0;
|
||||||
|
await cursor.forEach(async (carArg) => {
|
||||||
|
counter++;
|
||||||
|
counter % 50 === 0 ? console.log(`50 more of ${carArg.color}`) : null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// =======================================
|
// =======================================
|
||||||
// close the database connection
|
// close the database connection
|
||||||
// =======================================
|
// =======================================
|
||||||
tap.test('should close the database connection', async (tools) => {
|
tap.test('close', async () => {
|
||||||
await testDb.close();
|
if (smartmongoInstance) {
|
||||||
try {
|
await smartmongoInstance.stopAndDumpToDir('./.nogit/dbdump/test.ts');
|
||||||
await mongod.stop();
|
} else {
|
||||||
} catch (e) {}
|
await testDb.mongoDb.dropDatabase();
|
||||||
|
await testDb.close();
|
||||||
|
}
|
||||||
|
setTimeout(() => process.exit(), 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start({ throwOnError: true });
|
tap.start({ throwOnError: true });
|
||||||
|
95
test/test.typescript.ts
Normal file
95
test/test.typescript.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as smartmongo from '@push.rocks/smartmongo';
|
||||||
|
import { smartunique } from '../ts/smartdata.plugins.js';
|
||||||
|
|
||||||
|
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
|
||||||
|
|
||||||
|
console.log(process.memoryUsage());
|
||||||
|
|
||||||
|
// the tested module
|
||||||
|
import * as smartdata from '../ts/index.js';
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Connecting to the database server
|
||||||
|
// =======================================
|
||||||
|
|
||||||
|
let smartmongoInstance: smartmongo.SmartMongo;
|
||||||
|
let testDb: smartdata.SmartdataDb;
|
||||||
|
|
||||||
|
const totalCars = 2000;
|
||||||
|
|
||||||
|
tap.test('should create a testinstance as database', async () => {
|
||||||
|
smartmongoInstance = await smartmongo.SmartMongo.createAndStart();
|
||||||
|
testDb = new smartdata.SmartdataDb(await smartmongoInstance.getMongoDescriptor());
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.skip.test('should connect to atlas', async (tools) => {
|
||||||
|
const databaseName = `test-smartdata-${smartunique.shortId()}`;
|
||||||
|
testDb = new smartdata.SmartdataDb({
|
||||||
|
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGO_URL'),
|
||||||
|
mongoDbName: databaseName,
|
||||||
|
});
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// The actual tests
|
||||||
|
// =======================================
|
||||||
|
|
||||||
|
// ------
|
||||||
|
// Collections
|
||||||
|
// ------
|
||||||
|
@smartdata.Manager()
|
||||||
|
class Car extends smartdata.SmartDataDbDoc<Car, Car> {
|
||||||
|
@smartdata.unI()
|
||||||
|
public index: string = smartunique.shortId();
|
||||||
|
|
||||||
|
@smartdata.svDb()
|
||||||
|
public color: string;
|
||||||
|
|
||||||
|
@smartdata.svDb()
|
||||||
|
public brand: string;
|
||||||
|
|
||||||
|
@smartdata.svDb()
|
||||||
|
deepData = {
|
||||||
|
sodeep: 'yes',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(colorArg: string, brandArg: string) {
|
||||||
|
super();
|
||||||
|
this.color = colorArg;
|
||||||
|
this.brand = brandArg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCarClass = (dbArg: smartdata.SmartdataDb) => {
|
||||||
|
smartdata.setDefaultManagerForDoc({ db: dbArg }, Car);
|
||||||
|
return Car;
|
||||||
|
};
|
||||||
|
|
||||||
|
tap.test('should produce a car', async () => {
|
||||||
|
const CarClass = createCarClass(testDb);
|
||||||
|
const carInstance = new CarClass('red', 'Mercedes');
|
||||||
|
await carInstance.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should get a car', async () => {
|
||||||
|
const car = Car.getInstance({
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// close the database connection
|
||||||
|
// =======================================
|
||||||
|
tap.test('close', async () => {
|
||||||
|
await testDb.mongoDb.dropDatabase();
|
||||||
|
await testDb.close();
|
||||||
|
if (smartmongoInstance) {
|
||||||
|
await smartmongoInstance.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start({ throwOnError: true });
|
74
test/test.watch.ts
Normal file
74
test/test.watch.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
|
import * as smartmongo from '@push.rocks/smartmongo';
|
||||||
|
import { smartunique } from '../ts/smartdata.plugins.js';
|
||||||
|
|
||||||
|
const testQenv = new Qenv(process.cwd(), process.cwd() + '/.nogit/');
|
||||||
|
|
||||||
|
console.log(process.memoryUsage());
|
||||||
|
|
||||||
|
// the tested module
|
||||||
|
import * as smartdata from '../ts/index.js';
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Connecting to the database server
|
||||||
|
// =======================================
|
||||||
|
|
||||||
|
let smartmongoInstance: smartmongo.SmartMongo;
|
||||||
|
let testDb: smartdata.SmartdataDb;
|
||||||
|
|
||||||
|
const totalCars = 2000;
|
||||||
|
|
||||||
|
tap.test('should create a testinstance as database', async () => {
|
||||||
|
smartmongoInstance = await smartmongo.SmartMongo.createAndStart();
|
||||||
|
testDb = new smartdata.SmartdataDb(await smartmongoInstance.getMongoDescriptor());
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.skip.test('should connect to atlas', async (tools) => {
|
||||||
|
const databaseName = `test-smartdata-${smartunique.shortId()}`;
|
||||||
|
testDb = new smartdata.SmartdataDb({
|
||||||
|
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGO_URL'),
|
||||||
|
mongoDbName: databaseName,
|
||||||
|
});
|
||||||
|
await testDb.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
@smartdata.Collection(() => testDb)
|
||||||
|
class House extends smartdata.SmartDataDbDoc<House, House> {
|
||||||
|
@smartdata.unI()
|
||||||
|
public id: string = smartunique.shortId();
|
||||||
|
|
||||||
|
@smartdata.svDb()
|
||||||
|
public data = {
|
||||||
|
id: smartunique.shortId(),
|
||||||
|
hello: 'hello',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tap.test('should watch a collection', async (toolsArg) => {
|
||||||
|
const done = toolsArg.defer();
|
||||||
|
const watcher = await House.watch({});
|
||||||
|
watcher.changeSubject.subscribe(async (houseArg) => {
|
||||||
|
console.log('hey there, we observed a house');
|
||||||
|
await watcher.close();
|
||||||
|
done.resolve();
|
||||||
|
});
|
||||||
|
const newHouse = new House();
|
||||||
|
await newHouse.save();
|
||||||
|
console.log('saved a house');
|
||||||
|
await done.promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// close the database connection
|
||||||
|
// =======================================
|
||||||
|
tap.test('close', async () => {
|
||||||
|
await testDb.mongoDb.dropDatabase();
|
||||||
|
await testDb.close();
|
||||||
|
if (smartmongoInstance) {
|
||||||
|
await smartmongoInstance.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.start({ throwOnError: true });
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@push.rocks/smartdata',
|
||||||
|
version: '5.3.0',
|
||||||
|
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.'
|
||||||
|
}
|
17
ts/index.ts
17
ts/index.ts
@ -1,5 +1,14 @@
|
|||||||
export * from './smartdata.classes.db';
|
export * from './smartdata.classes.db.js';
|
||||||
export * from './smartdata.classes.collection';
|
export * from './smartdata.classes.collection.js';
|
||||||
export * from './smartdata.classes.doc';
|
export * from './smartdata.classes.doc.js';
|
||||||
|
export * from './smartdata.classes.easystore.js';
|
||||||
|
export * from './smartdata.classes.cursor.js';
|
||||||
|
|
||||||
export { IMongoDescriptor } from './interfaces';
|
import * as convenience from './smartadata.convenience.js';
|
||||||
|
|
||||||
|
export { convenience };
|
||||||
|
|
||||||
|
// to be removed with the next breaking update
|
||||||
|
import type * as plugins from './smartdata.plugins.js';
|
||||||
|
type IMongoDescriptor = plugins.tsclass.database.IMongoDescriptor;
|
||||||
|
export type { IMongoDescriptor };
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from './mongodescriptor';
|
|
@ -1,22 +0,0 @@
|
|||||||
export interface IMongoDescriptor {
|
|
||||||
/**
|
|
||||||
* the URL to connect to
|
|
||||||
*/
|
|
||||||
mongoDbUrl: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* the db to use for the project
|
|
||||||
*/
|
|
||||||
mongoDbName?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* a username to use to connect to the database
|
|
||||||
*/
|
|
||||||
|
|
||||||
mongoDbUser?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* an optional password that will be replace <PASSWORD> in the connection string
|
|
||||||
*/
|
|
||||||
mongoDbPass?: string;
|
|
||||||
}
|
|
5
ts/smartadata.convenience.ts
Normal file
5
ts/smartadata.convenience.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
|
||||||
|
export const getNewUniqueId = async (prefixArg?: string) => {
|
||||||
|
return plugins.smartunique.uni(prefixArg);
|
||||||
|
};
|
@ -1,7 +1,9 @@
|
|||||||
import * as plugins from './smartdata.plugins';
|
import * as plugins from './smartdata.plugins.js';
|
||||||
import { SmartdataDb } from './smartdata.classes.db';
|
import { SmartdataDb } from './smartdata.classes.db.js';
|
||||||
import { SmartDataDbDoc } from './smartdata.classes.doc';
|
import { SmartdataDbCursor } from './smartdata.classes.cursor.js';
|
||||||
import { CollectionFactory } from './smartdata.classes.collectionfactory';
|
import { SmartDataDbDoc } from './smartdata.classes.doc.js';
|
||||||
|
import { SmartdataDbWatcher } from './smartdata.classes.watcher.js';
|
||||||
|
import { CollectionFactory } from './smartdata.classes.collectionfactory.js';
|
||||||
|
|
||||||
export interface IFindOptions {
|
export interface IFindOptions {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@ -14,7 +16,7 @@ export interface IDocValidationFunc<T> {
|
|||||||
(doc: T): boolean;
|
(doc: T): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TDelayedDbCreation = () => SmartdataDb;
|
export type TDelayed<TDelayedArg> = () => TDelayedArg;
|
||||||
|
|
||||||
const collectionFactory = new CollectionFactory();
|
const collectionFactory = new CollectionFactory();
|
||||||
|
|
||||||
@ -22,20 +24,100 @@ const collectionFactory = new CollectionFactory();
|
|||||||
* This is a decorator that will tell the decorated class what dbTable to use
|
* This is a decorator that will tell the decorated class what dbTable to use
|
||||||
* @param dbArg
|
* @param dbArg
|
||||||
*/
|
*/
|
||||||
export function Collection(dbArg: SmartdataDb | TDelayedDbCreation) {
|
export function Collection(dbArg: SmartdataDb | TDelayed<SmartdataDb>) {
|
||||||
return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
|
return function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
|
||||||
return class extends constructor {
|
const decoratedClass = class extends constructor {
|
||||||
|
public static className = constructor.name;
|
||||||
public static get collection() {
|
public static get collection() {
|
||||||
|
if (!(dbArg instanceof SmartdataDb)) {
|
||||||
|
dbArg = dbArg();
|
||||||
|
}
|
||||||
return collectionFactory.getCollection(constructor.name, dbArg);
|
return collectionFactory.getCollection(constructor.name, dbArg);
|
||||||
}
|
}
|
||||||
public get collection() {
|
public get collection() {
|
||||||
|
if (!(dbArg instanceof SmartdataDb)) {
|
||||||
|
dbArg = dbArg();
|
||||||
|
}
|
||||||
return collectionFactory.getCollection(constructor.name, dbArg);
|
return collectionFactory.getCollection(constructor.name, dbArg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
return decoratedClass;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line: max-classes-per-file
|
export interface IManager {
|
||||||
|
db: SmartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setDefaultManagerForDoc = <T>(managerArg: IManager, dbDocArg: T): T => {
|
||||||
|
(dbDocArg as any).prototype.defaultManager = managerArg;
|
||||||
|
return dbDocArg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a decorator that will tell the decorated class what dbTable to use
|
||||||
|
* @param dbArg
|
||||||
|
*/
|
||||||
|
export function managed<TManager extends IManager>(managerArg?: TManager | TDelayed<TManager>) {
|
||||||
|
return function classDecorator<T extends { new (...args: any[]): any }>(constructor: T) {
|
||||||
|
const decoratedClass = class extends constructor {
|
||||||
|
public static className = constructor.name;
|
||||||
|
public static get collection() {
|
||||||
|
let dbArg: SmartdataDb;
|
||||||
|
if (!managerArg) {
|
||||||
|
dbArg = this.prototype.defaultManager.db;
|
||||||
|
} else if (managerArg['db']) {
|
||||||
|
dbArg = (managerArg as TManager).db;
|
||||||
|
} else {
|
||||||
|
dbArg = (managerArg as TDelayed<TManager>)().db;
|
||||||
|
}
|
||||||
|
return collectionFactory.getCollection(constructor.name, dbArg);
|
||||||
|
}
|
||||||
|
public get collection() {
|
||||||
|
let dbArg: SmartdataDb;
|
||||||
|
if (!managerArg) {
|
||||||
|
//console.log(this.defaultManager.db);
|
||||||
|
//process.exit(0)
|
||||||
|
dbArg = this.defaultManager.db;
|
||||||
|
} else if (managerArg['db']) {
|
||||||
|
dbArg = (managerArg as TManager).db;
|
||||||
|
} else {
|
||||||
|
dbArg = (managerArg as TDelayed<TManager>)().db;
|
||||||
|
}
|
||||||
|
return collectionFactory.getCollection(constructor.name, dbArg);
|
||||||
|
}
|
||||||
|
public static get manager() {
|
||||||
|
let manager: TManager;
|
||||||
|
if (!managerArg) {
|
||||||
|
manager = this.prototype.defaultManager;
|
||||||
|
} else if (managerArg['db']) {
|
||||||
|
manager = managerArg as TManager;
|
||||||
|
} else {
|
||||||
|
manager = (managerArg as TDelayed<TManager>)();
|
||||||
|
}
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
public get manager() {
|
||||||
|
let manager: TManager;
|
||||||
|
if (!managerArg) {
|
||||||
|
manager = this.defaultManager;
|
||||||
|
} else if (managerArg['db']) {
|
||||||
|
manager = managerArg as TManager;
|
||||||
|
} else {
|
||||||
|
manager = (managerArg as TDelayed<TManager>)();
|
||||||
|
}
|
||||||
|
return manager;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return decoratedClass;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dpecrecated use @managed instead
|
||||||
|
*/
|
||||||
|
export const Manager = managed;
|
||||||
|
|
||||||
export class SmartdataCollection<T> {
|
export class SmartdataCollection<T> {
|
||||||
/**
|
/**
|
||||||
* the collection that is used
|
* the collection that is used
|
||||||
@ -98,12 +180,57 @@ export class SmartdataCollection<T> {
|
|||||||
/**
|
/**
|
||||||
* finds an object in the DbCollection
|
* finds an object in the DbCollection
|
||||||
*/
|
*/
|
||||||
public async find(filterObject: any): Promise<any> {
|
public async findOne(filterObject: any): Promise<any> {
|
||||||
await this.init();
|
await this.init();
|
||||||
const result = await this.mongoDbCollection.find(filterObject).toArray();
|
const cursor = this.mongoDbCollection.find(filterObject);
|
||||||
|
const result = await cursor.next();
|
||||||
|
cursor.close();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCursor(
|
||||||
|
filterObjectArg: any,
|
||||||
|
dbDocArg: typeof SmartDataDbDoc
|
||||||
|
): Promise<SmartdataDbCursor<any>> {
|
||||||
|
await this.init();
|
||||||
|
const cursor = this.mongoDbCollection.find(filterObjectArg);
|
||||||
|
return new SmartdataDbCursor(cursor, dbDocArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finds an object in the DbCollection
|
||||||
|
*/
|
||||||
|
public async findAll(filterObject: any): Promise<any[]> {
|
||||||
|
await this.init();
|
||||||
|
const cursor = this.mongoDbCollection.find(filterObject);
|
||||||
|
const result = await cursor.toArray();
|
||||||
|
cursor.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* watches the collection while applying a filter
|
||||||
|
*/
|
||||||
|
public async watch(
|
||||||
|
filterObject: any,
|
||||||
|
smartdataDbDocArg: typeof SmartDataDbDoc
|
||||||
|
): Promise<SmartdataDbWatcher> {
|
||||||
|
await this.init();
|
||||||
|
const changeStream = this.mongoDbCollection.watch(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$match: filterObject,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
fullDocument: 'updateLookup',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const smartdataWatcher = new SmartdataDbWatcher(changeStream, smartdataDbDocArg);
|
||||||
|
await smartdataWatcher.readyDeferred.promise;
|
||||||
|
return smartdataWatcher;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create an object in the database
|
* create an object in the database
|
||||||
*/
|
*/
|
||||||
@ -143,9 +270,12 @@ export class SmartdataCollection<T> {
|
|||||||
await this.init();
|
await this.init();
|
||||||
await this.checkDoc(dbDocArg);
|
await this.checkDoc(dbDocArg);
|
||||||
const identifiableObject = await dbDocArg.createIdentifiableObject();
|
const identifiableObject = await dbDocArg.createIdentifiableObject();
|
||||||
await this.mongoDbCollection.deleteOne(identifiableObject, {
|
await this.mongoDbCollection.deleteOne(identifiableObject);
|
||||||
w: 1,
|
}
|
||||||
});
|
|
||||||
|
public async getCount(filterObject: any) {
|
||||||
|
await this.init();
|
||||||
|
return this.mongoDbCollection.countDocuments(filterObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,7 +283,7 @@ export class SmartdataCollection<T> {
|
|||||||
* if this.objectValidation is not set it passes.
|
* if this.objectValidation is not set it passes.
|
||||||
*/
|
*/
|
||||||
private checkDoc(docArg: T): Promise<void> {
|
private checkDoc(docArg: T): Promise<void> {
|
||||||
const done = plugins.smartq.defer<void>();
|
const done = plugins.smartpromise.defer<void>();
|
||||||
let validationResult = true;
|
let validationResult = true;
|
||||||
if (this.objectValidation) {
|
if (this.objectValidation) {
|
||||||
validationResult = this.objectValidation(docArg);
|
validationResult = this.objectValidation(docArg);
|
||||||
|
@ -1,22 +1,16 @@
|
|||||||
import * as plugins from './smartdata.plugins';
|
import * as plugins from './smartdata.plugins.js';
|
||||||
import { SmartdataCollection } from './smartdata.classes.collection';
|
import { SmartdataCollection } from './smartdata.classes.collection.js';
|
||||||
import { SmartdataDb } from './smartdata.classes.db';
|
import { SmartdataDb } from './smartdata.classes.db.js';
|
||||||
|
|
||||||
export class CollectionFactory {
|
export class CollectionFactory {
|
||||||
public collections: { [key: string]: SmartdataCollection<any> } = {};
|
public collections: { [key: string]: SmartdataCollection<any> } = {};
|
||||||
|
|
||||||
public getCollection = (
|
public getCollection = (nameArg: string, dbArg: SmartdataDb): SmartdataCollection<any> => {
|
||||||
nameArg: string,
|
|
||||||
dbArg: SmartdataDb | (() => SmartdataDb)
|
|
||||||
): SmartdataCollection<any> => {
|
|
||||||
if (!this.collections[nameArg]) {
|
if (!this.collections[nameArg]) {
|
||||||
this.collections[nameArg] = (() => {
|
this.collections[nameArg] = (() => {
|
||||||
if (dbArg instanceof SmartdataDb) {
|
if (dbArg instanceof SmartdataDb) {
|
||||||
// tslint:disable-next-line: no-string-literal
|
// tslint:disable-next-line: no-string-literal
|
||||||
return new SmartdataCollection(nameArg, dbArg);
|
return new SmartdataCollection(nameArg, dbArg);
|
||||||
} else {
|
|
||||||
dbArg = dbArg();
|
|
||||||
return new SmartdataCollection(nameArg, dbArg);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
46
ts/smartdata.classes.cursor.ts
Normal file
46
ts/smartdata.classes.cursor.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { SmartDataDbDoc } from './smartdata.classes.doc.js';
|
||||||
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a wrapper for the native mongodb cursor. Exposes better
|
||||||
|
*/
|
||||||
|
export class SmartdataDbCursor<T = any> {
|
||||||
|
// STATIC
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public mongodbCursor: plugins.mongodb.FindCursor<T>;
|
||||||
|
private smartdataDbDoc: typeof SmartDataDbDoc;
|
||||||
|
constructor(cursorArg: plugins.mongodb.FindCursor<T>, dbDocArg: typeof SmartDataDbDoc) {
|
||||||
|
this.mongodbCursor = cursorArg;
|
||||||
|
this.smartdataDbDoc = dbDocArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async next(closeAtEnd = true) {
|
||||||
|
const result = this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(
|
||||||
|
await this.mongodbCursor.next()
|
||||||
|
);
|
||||||
|
if (!result && closeAtEnd) {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async forEach(forEachFuncArg: (itemArg: T) => Promise<any>, closeCursorAtEnd = true) {
|
||||||
|
let nextDocument: any;
|
||||||
|
do {
|
||||||
|
nextDocument = await this.mongodbCursor.next();
|
||||||
|
if (nextDocument) {
|
||||||
|
const nextClassInstance =
|
||||||
|
this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(nextDocument);
|
||||||
|
await forEachFuncArg(nextClassInstance as any);
|
||||||
|
}
|
||||||
|
} while (nextDocument);
|
||||||
|
if (closeCursorAtEnd) {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close() {
|
||||||
|
await this.mongodbCursor.close();
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
import * as plugins from './smartdata.plugins';
|
import * as plugins from './smartdata.plugins.js';
|
||||||
import { ObjectMap } from '@pushrocks/lik';
|
|
||||||
|
|
||||||
import { SmartdataCollection } from './smartdata.classes.collection';
|
import { SmartdataCollection } from './smartdata.classes.collection.js';
|
||||||
|
import { EasyStore } from './smartdata.classes.easystore.js';
|
||||||
|
|
||||||
import { logger } from './smartdata.logging';
|
import { logger } from './smartdata.logging.js';
|
||||||
import { IMongoDescriptor } from './interfaces';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interface - indicates the connection status of the db
|
* interface - indicates the connection status of the db
|
||||||
@ -12,17 +11,24 @@ import { IMongoDescriptor } from './interfaces';
|
|||||||
export type TConnectionStatus = 'initial' | 'disconnected' | 'connected' | 'failed';
|
export type TConnectionStatus = 'initial' | 'disconnected' | 'connected' | 'failed';
|
||||||
|
|
||||||
export class SmartdataDb {
|
export class SmartdataDb {
|
||||||
smartdataOptions: IMongoDescriptor;
|
smartdataOptions: plugins.tsclass.database.IMongoDescriptor;
|
||||||
mongoDbClient: plugins.mongodb.MongoClient;
|
mongoDbClient: plugins.mongodb.MongoClient;
|
||||||
mongoDb: plugins.mongodb.Db;
|
mongoDb: plugins.mongodb.Db;
|
||||||
status: TConnectionStatus;
|
status: TConnectionStatus;
|
||||||
smartdataCollectionMap = new ObjectMap<SmartdataCollection<any>>();
|
statusConnectedDeferred = plugins.smartpromise.defer();
|
||||||
|
smartdataCollectionMap = new plugins.lik.ObjectMap<SmartdataCollection<any>>();
|
||||||
|
|
||||||
constructor(smartdataOptions: IMongoDescriptor) {
|
constructor(smartdataOptions: plugins.tsclass.database.IMongoDescriptor) {
|
||||||
this.smartdataOptions = smartdataOptions;
|
this.smartdataOptions = smartdataOptions;
|
||||||
this.status = 'initial';
|
this.status = 'initial';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// easystore
|
||||||
|
public async createEasyStore(nameIdArg: string) {
|
||||||
|
const easyStore = new EasyStore(nameIdArg, this);
|
||||||
|
return easyStore;
|
||||||
|
}
|
||||||
|
|
||||||
// basic connection stuff ----------------------------------------------
|
// basic connection stuff ----------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,13 +46,12 @@ export class SmartdataDb {
|
|||||||
.replace('<dbname>', this.smartdataOptions.mongoDbName);
|
.replace('<dbname>', this.smartdataOptions.mongoDbName);
|
||||||
|
|
||||||
this.mongoDbClient = await plugins.mongodb.MongoClient.connect(finalConnectionUrl, {
|
this.mongoDbClient = await plugins.mongodb.MongoClient.connect(finalConnectionUrl, {
|
||||||
useNewUrlParser: true,
|
|
||||||
useUnifiedTopology: true,
|
|
||||||
maxPoolSize: 100,
|
maxPoolSize: 100,
|
||||||
maxIdleTimeMS: 10,
|
maxIdleTimeMS: 10,
|
||||||
});
|
});
|
||||||
this.mongoDb = this.mongoDbClient.db(this.smartdataOptions.mongoDbName);
|
this.mongoDb = this.mongoDbClient.db(this.smartdataOptions.mongoDbName);
|
||||||
this.status = 'connected';
|
this.status = 'connected';
|
||||||
|
this.statusConnectedDeferred.resolve();
|
||||||
console.log(`Connected to database ${this.smartdataOptions.mongoDbName}`);
|
console.log(`Connected to database ${this.smartdataOptions.mongoDbName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +76,7 @@ export class SmartdataDb {
|
|||||||
* @returns DbTable
|
* @returns DbTable
|
||||||
*/
|
*/
|
||||||
public async getSmartdataCollectionByName<T>(nameArg: string): Promise<SmartdataCollection<T>> {
|
public async getSmartdataCollectionByName<T>(nameArg: string): Promise<SmartdataCollection<T>> {
|
||||||
const resultCollection = this.smartdataCollectionMap.find((dbTableArg) => {
|
const resultCollection = await this.smartdataCollectionMap.find(async (dbTableArg) => {
|
||||||
return dbTableArg.collectionName === nameArg;
|
return dbTableArg.collectionName === nameArg;
|
||||||
});
|
});
|
||||||
return resultCollection;
|
return resultCollection;
|
||||||
|
304
ts/smartdata.classes.distributedcoordinator.ts
Normal file
304
ts/smartdata.classes.distributedcoordinator.ts
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
import { SmartdataDb } from './smartdata.classes.db.js';
|
||||||
|
import { managed, setDefaultManagerForDoc } from './smartdata.classes.collection.js';
|
||||||
|
import { SmartDataDbDoc, svDb, unI } from './smartdata.classes.doc.js';
|
||||||
|
import { SmartdataDbWatcher } from './smartdata.classes.watcher.js';
|
||||||
|
|
||||||
|
@managed()
|
||||||
|
export class DistributedClass extends SmartDataDbDoc<DistributedClass, DistributedClass> {
|
||||||
|
// INSTANCE
|
||||||
|
@unI()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public data: {
|
||||||
|
status: 'initializing' | 'bidding' | 'settled' | 'stopped';
|
||||||
|
biddingShortcode?: string;
|
||||||
|
biddingStartTime?: number;
|
||||||
|
lastUpdated: number;
|
||||||
|
elected: boolean;
|
||||||
|
/**
|
||||||
|
* used to store request
|
||||||
|
*/
|
||||||
|
taskRequests: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only used by the leader to convey consultation results
|
||||||
|
*/
|
||||||
|
taskRequestResults: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequestResult[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file implements a distributed coordinator according to the @pushrocks/taskbuffer standard.
|
||||||
|
* you should not set up this yourself. Instead, there is a factory on the SmartdataDb class
|
||||||
|
* that will take care of setting this up.
|
||||||
|
*/
|
||||||
|
export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distributedCoordination
|
||||||
|
.AbstractDistributedCoordinator {
|
||||||
|
public readyPromise: Promise<any>;
|
||||||
|
public db: SmartdataDb;
|
||||||
|
private asyncExecutionStack = new plugins.lik.AsyncExecutionStack();
|
||||||
|
public ownInstance: DistributedClass;
|
||||||
|
public distributedWatcher: SmartdataDbWatcher<DistributedClass>;
|
||||||
|
|
||||||
|
constructor(dbArg: SmartdataDb) {
|
||||||
|
super();
|
||||||
|
this.db = dbArg;
|
||||||
|
setDefaultManagerForDoc(this, DistributedClass);
|
||||||
|
this.readyPromise = this.db.statusConnectedDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// smartdata specific stuff
|
||||||
|
public async start() {
|
||||||
|
await this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
if (this.distributedWatcher) {
|
||||||
|
await this.distributedWatcher.close();
|
||||||
|
}
|
||||||
|
if (this.ownInstance?.data.elected) {
|
||||||
|
this.ownInstance.data.elected = false;
|
||||||
|
}
|
||||||
|
if (this.ownInstance?.data.status === 'stopped') {
|
||||||
|
console.log(`stopping a distributed instance that has not been started yet.`);
|
||||||
|
}
|
||||||
|
this.ownInstance.data.status = 'stopped';
|
||||||
|
await this.ownInstance.save();
|
||||||
|
console.log(`stopped ${this.ownInstance.id}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public id = plugins.smartunique.uni('distributedInstance');
|
||||||
|
|
||||||
|
private startHeartbeat = async () => {
|
||||||
|
while (this.ownInstance.data.status !== 'stopped') {
|
||||||
|
await this.sendHeartbeat();
|
||||||
|
await plugins.smartdelay.delayForRandom(5000, 10000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public async sendHeartbeat() {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
if (this.ownInstance.data.status === 'stopped') {
|
||||||
|
console.log(`aborted sending heartbeat because status is stopped`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.ownInstance.updateFromDb();
|
||||||
|
this.ownInstance.data.lastUpdated = Date.now();
|
||||||
|
await this.ownInstance.save();
|
||||||
|
console.log(`sent heartbeat for ${this.ownInstance.id}`);
|
||||||
|
const allInstances = DistributedClass.getInstances({});
|
||||||
|
});
|
||||||
|
if (this.ownInstance.data.status === 'stopped') {
|
||||||
|
console.log(`aborted sending heartbeat because status is stopped`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const eligibleLeader = await this.getEligibleLeader();
|
||||||
|
// not awaiting here because we don't want to block the heartbeat
|
||||||
|
this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
if (!eligibleLeader && this.ownInstance.data.status === 'settled') {
|
||||||
|
this.checkAndMaybeLead();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private async init() {
|
||||||
|
await this.readyPromise;
|
||||||
|
if (!this.ownInstance) {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
this.ownInstance = new DistributedClass();
|
||||||
|
this.ownInstance.id = this.id;
|
||||||
|
this.ownInstance.data = {
|
||||||
|
elected: false,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
status: 'initializing',
|
||||||
|
taskRequests: [],
|
||||||
|
taskRequestResults: [],
|
||||||
|
};
|
||||||
|
await this.ownInstance.save();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`distributed instance already initialized`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lets enable the heartbeat
|
||||||
|
this.startHeartbeat();
|
||||||
|
|
||||||
|
// lets do a leader check
|
||||||
|
await this.checkAndMaybeLead();
|
||||||
|
|
||||||
|
return this.ownInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEligibleLeader() {
|
||||||
|
return this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
const allInstances = await DistributedClass.getInstances({});
|
||||||
|
let leaders = allInstances.filter((instanceArg) => instanceArg.data.elected === true);
|
||||||
|
const eligibleLeader = leaders.find(
|
||||||
|
(leader) =>
|
||||||
|
leader.data.lastUpdated >=
|
||||||
|
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 20 })
|
||||||
|
);
|
||||||
|
return eligibleLeader;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --> leader election
|
||||||
|
public async checkAndMaybeLead() {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
this.ownInstance.data.status = 'initializing';
|
||||||
|
this.ownInstance.save();
|
||||||
|
});
|
||||||
|
if (await this.getEligibleLeader()) {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
await this.ownInstance.updateFromDb();
|
||||||
|
this.ownInstance.data.status = 'settled';
|
||||||
|
await this.ownInstance.save();
|
||||||
|
console.log(`${this.ownInstance.id} settled as follower`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
(await DistributedClass.getInstances({})).find((instanceArg) => {
|
||||||
|
instanceArg.data.status === 'bidding' &&
|
||||||
|
instanceArg.data.biddingStartTime <= Date.now() - 4000 &&
|
||||||
|
instanceArg.data.biddingStartTime >= Date.now() - 30000;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
console.log('too late to the bidding party... waiting for next round.');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
await this.ownInstance.updateFromDb();
|
||||||
|
this.ownInstance.data.status = 'bidding';
|
||||||
|
this.ownInstance.data.biddingStartTime = Date.now();
|
||||||
|
this.ownInstance.data.biddingShortcode = plugins.smartunique.shortId();
|
||||||
|
await this.ownInstance.save();
|
||||||
|
console.log('bidding code stored.');
|
||||||
|
});
|
||||||
|
console.log(`bidding for leadership...`);
|
||||||
|
await plugins.smartdelay.delayFor(
|
||||||
|
plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 })
|
||||||
|
);
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
let biddingInstances = await DistributedClass.getInstances({});
|
||||||
|
biddingInstances = biddingInstances.filter(
|
||||||
|
(instanceArg) =>
|
||||||
|
instanceArg.data.status === 'bidding' &&
|
||||||
|
instanceArg.data.lastUpdated >=
|
||||||
|
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 10 })
|
||||||
|
);
|
||||||
|
console.log(`found ${biddingInstances.length} bidding instances...`);
|
||||||
|
this.ownInstance.data.elected = true;
|
||||||
|
for (const biddingInstance of biddingInstances) {
|
||||||
|
if (biddingInstance.data.biddingShortcode < this.ownInstance.data.biddingShortcode) {
|
||||||
|
this.ownInstance.data.elected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await plugins.smartdelay.delayFor(5000);
|
||||||
|
console.log(`settling with status elected = ${this.ownInstance.data.elected}`);
|
||||||
|
this.ownInstance.data.status = 'settled';
|
||||||
|
await this.ownInstance.save();
|
||||||
|
});
|
||||||
|
if (this.ownInstance.data.elected) {
|
||||||
|
this.leadFunction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* when it has been determined
|
||||||
|
* that this instance is leading
|
||||||
|
* the leading is implemented here
|
||||||
|
*/
|
||||||
|
public async leadFunction() {
|
||||||
|
this.distributedWatcher = await DistributedClass.watch({});
|
||||||
|
|
||||||
|
const currentTaskRequests: Array<{
|
||||||
|
taskName: string;
|
||||||
|
taskExecutionTime: number;
|
||||||
|
/**
|
||||||
|
* all instances that requested this task
|
||||||
|
*/
|
||||||
|
requestingDistibutedInstanceIds: string[];
|
||||||
|
responseTimeout: plugins.smartdelay.Timeout<any>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
this.distributedWatcher.changeSubject.subscribe({
|
||||||
|
next: async (distributedDoc) => {
|
||||||
|
if (!distributedDoc) {
|
||||||
|
console.log(`registered deletion of instance...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(distributedDoc);
|
||||||
|
console.log(`registered change for ${distributedDoc.id}`);
|
||||||
|
distributedDoc;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
while (this.ownInstance.data.status !== 'stopped' && this.ownInstance.data.elected) {
|
||||||
|
const allInstances = await DistributedClass.getInstances({});
|
||||||
|
for (const instance of allInstances) {
|
||||||
|
if (instance.data.status === 'stopped') {
|
||||||
|
await instance.delete();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await plugins.smartdelay.delayFor(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// abstract implemented methods
|
||||||
|
public async fireDistributedTaskRequest(
|
||||||
|
taskRequestArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest
|
||||||
|
): Promise<plugins.taskbuffer.distributedCoordination.IDistributedTaskRequestResult> {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
if (!this.ownInstance) {
|
||||||
|
console.error('instance need to be started first...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.ownInstance.updateFromDb();
|
||||||
|
this.ownInstance.data.taskRequests.push(taskRequestArg);
|
||||||
|
await this.ownInstance.save();
|
||||||
|
});
|
||||||
|
await plugins.smartdelay.delayFor(10000);
|
||||||
|
const result = await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
await this.ownInstance.updateFromDb();
|
||||||
|
const taskRequestResult = this.ownInstance.data.taskRequestResults.find((resultItem) => {
|
||||||
|
return resultItem.requestResponseId === taskRequestArg.requestResponseId;
|
||||||
|
});
|
||||||
|
return taskRequestResult;
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
console.warn('no result found for task request...');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateDistributedTaskRequest(
|
||||||
|
infoBasisArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest
|
||||||
|
): Promise<void> {
|
||||||
|
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
const existingInfoBasis = this.ownInstance.data.taskRequests.find((infoBasisItem) => {
|
||||||
|
return (
|
||||||
|
infoBasisItem.taskName === infoBasisArg.taskName &&
|
||||||
|
infoBasisItem.taskExecutionTime === infoBasisArg.taskExecutionTime
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (!existingInfoBasis) {
|
||||||
|
console.warn('trying to update a non existing task request... aborting!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(existingInfoBasis, infoBasisArg);
|
||||||
|
await this.ownInstance.save();
|
||||||
|
plugins.smartdelay.delayFor(60000).then(() => {
|
||||||
|
this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
|
||||||
|
const indexToRemove = this.ownInstance.data.taskRequests.indexOf(existingInfoBasis);
|
||||||
|
this.ownInstance.data.taskRequests.splice(indexToRemove, indexToRemove);
|
||||||
|
await this.ownInstance.save();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,22 @@
|
|||||||
import * as plugins from './smartdata.plugins';
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
|
||||||
import { ObjectMap } from '@pushrocks/lik';
|
import { SmartdataDb } from './smartdata.classes.db.js';
|
||||||
|
import { SmartdataDbCursor } from './smartdata.classes.cursor.js';
|
||||||
import { SmartdataDb } from './smartdata.classes.db';
|
import { type IManager, SmartdataCollection } from './smartdata.classes.collection.js';
|
||||||
import { SmartdataCollection } from './smartdata.classes.collection';
|
import { SmartdataDbWatcher } from './smartdata.classes.watcher.js';
|
||||||
|
|
||||||
export type TDocCreation = 'db' | 'new' | 'mixed';
|
export type TDocCreation = 'db' | 'new' | 'mixed';
|
||||||
|
|
||||||
|
export function globalSvDb() {
|
||||||
|
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
|
||||||
|
console.log(`called svDb() on >${target.constructor.name}.${key}<`);
|
||||||
|
if (!target.globalSaveableProperties) {
|
||||||
|
target.globalSaveableProperties = [];
|
||||||
|
}
|
||||||
|
target.globalSaveableProperties.push(key);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* saveable - saveable decorator to be used on class properties
|
* saveable - saveable decorator to be used on class properties
|
||||||
*/
|
*/
|
||||||
@ -41,25 +51,193 @@ export function unI() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SmartDataDbDoc<T extends TImplements, TImplements> {
|
export const convertFilterForMongoDb = (filterArg: { [key: string]: any }) => {
|
||||||
|
const convertedFilter: { [key: string]: any } = {};
|
||||||
|
|
||||||
|
const convertFilterArgument = (keyPathArg2: string, filterArg2: any) => {
|
||||||
|
if (Array.isArray(filterArg2)) {
|
||||||
|
// Directly assign arrays (they might be using operators like $in or $all)
|
||||||
|
convertFilterArgument(keyPathArg2, filterArg2[0]);
|
||||||
|
} else if (typeof filterArg2 === 'object' && filterArg2 !== null) {
|
||||||
|
for (const key of Object.keys(filterArg2)) {
|
||||||
|
if (key.startsWith('$')) {
|
||||||
|
convertedFilter[keyPathArg2] = filterArg2;
|
||||||
|
return;
|
||||||
|
} else if (key.includes('.')) {
|
||||||
|
throw new Error('keys cannot contain dots');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(filterArg2)) {
|
||||||
|
convertFilterArgument(`${keyPathArg2}.${key}`, filterArg2[key]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
convertedFilter[keyPathArg2] = filterArg2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(filterArg)) {
|
||||||
|
convertFilterArgument(key, filterArg[key]);
|
||||||
|
}
|
||||||
|
return convertedFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends IManager = any> {
|
||||||
/**
|
/**
|
||||||
* the collection object an Doc belongs to
|
* the collection object an Doc belongs to
|
||||||
*/
|
*/
|
||||||
public static collection: SmartdataCollection<any>;
|
public static collection: SmartdataCollection<any>;
|
||||||
public collection: SmartdataCollection<any>;
|
public collection: SmartdataCollection<any>;
|
||||||
|
public static defaultManager;
|
||||||
|
public static manager;
|
||||||
|
public manager: TManager;
|
||||||
|
|
||||||
|
// STATIC
|
||||||
|
public static createInstanceFromMongoDbNativeDoc<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
mongoDbNativeDocArg: any
|
||||||
|
): T {
|
||||||
|
const newInstance = new this();
|
||||||
|
(newInstance as any).creationStatus = 'db';
|
||||||
|
for (const key of Object.keys(mongoDbNativeDocArg)) {
|
||||||
|
newInstance[key] = mongoDbNativeDocArg[key];
|
||||||
|
}
|
||||||
|
return newInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets all instances as array
|
||||||
|
* @param this
|
||||||
|
* @param filterArg
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public static async getInstances<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
||||||
|
): Promise<T[]> {
|
||||||
|
const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg));
|
||||||
|
const returnArray = [];
|
||||||
|
for (const foundDoc of foundDocs) {
|
||||||
|
const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc);
|
||||||
|
returnArray.push(newInstance);
|
||||||
|
}
|
||||||
|
return returnArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets the first matching instance
|
||||||
|
* @param this
|
||||||
|
* @param filterArg
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public static async getInstance<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
||||||
|
): Promise<T> {
|
||||||
|
const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg));
|
||||||
|
if (foundDoc) {
|
||||||
|
const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc);
|
||||||
|
return newInstance;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a unique id prefixed with the class name
|
||||||
|
*/
|
||||||
|
public static async getNewId<T = any>(this: plugins.tsclass.typeFest.Class<T>, lengthArg: number = 20) {
|
||||||
|
return `${(this as any).className}:${plugins.smartunique.shortId(lengthArg)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get cursor
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public static async getCursor<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
||||||
|
) {
|
||||||
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
|
const cursor: SmartdataDbCursor<T> = await collection.getCursor(
|
||||||
|
convertFilterForMongoDb(filterArg),
|
||||||
|
this as any as typeof SmartDataDbDoc
|
||||||
|
);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* watch the collection
|
||||||
|
* @param this
|
||||||
|
* @param filterArg
|
||||||
|
* @param forEachFunction
|
||||||
|
*/
|
||||||
|
public static async watch<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
||||||
|
) {
|
||||||
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
|
const watcher: SmartdataDbWatcher<T> = await collection.watch(
|
||||||
|
convertFilterForMongoDb(filterArg),
|
||||||
|
this as any
|
||||||
|
);
|
||||||
|
return watcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run a function for all instances
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public static async forEach<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||||
|
forEachFunction: (itemArg: T) => Promise<any>
|
||||||
|
) {
|
||||||
|
const cursor: SmartdataDbCursor<T> = await (this as any).getCursor(filterArg);
|
||||||
|
await cursor.forEach(forEachFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a count of the documents in the collection
|
||||||
|
*/
|
||||||
|
public static async getCount<T>(
|
||||||
|
this: plugins.tsclass.typeFest.Class<T>,
|
||||||
|
filterArg: plugins.tsclass.typeFest.PartialDeep<T> = ({} as any)
|
||||||
|
) {
|
||||||
|
const collection: SmartdataCollection<T> = (this as any).collection;
|
||||||
|
return await collection.getCount(filterArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* how the Doc in memory was created, may prove useful later.
|
* how the Doc in memory was created, may prove useful later.
|
||||||
*/
|
*/
|
||||||
public creationStatus: TDocCreation = 'new';
|
public creationStatus: TDocCreation = 'new';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updated from db in any case where doc comes from db
|
||||||
|
*/
|
||||||
|
@globalSvDb()
|
||||||
|
_createdAt: string = (new Date()).toISOString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* will be updated everytime the doc is saved
|
||||||
|
*/
|
||||||
|
@globalSvDb()
|
||||||
|
_updatedAt: string = (new Date()).toISOString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* an array of saveable properties of ALL doc
|
||||||
|
*/
|
||||||
|
public globalSaveableProperties: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* unique indexes
|
* unique indexes
|
||||||
*/
|
*/
|
||||||
public uniqueIndexes: string[];
|
public uniqueIndexes: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* an array of saveable properties of a doc
|
* an array of saveable properties of a specific doc
|
||||||
*/
|
*/
|
||||||
public saveableProperties: string[];
|
public saveableProperties: string[];
|
||||||
|
|
||||||
@ -78,54 +256,6 @@ export class SmartDataDbDoc<T extends TImplements, TImplements> {
|
|||||||
*/
|
*/
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
public static async getInstances<T>(
|
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
|
||||||
): Promise<T[]> {
|
|
||||||
const convertedFilter: any = {};
|
|
||||||
const convertFilterArgument = (keyPathArg: string, filterArg2: any) => {
|
|
||||||
if (typeof filterArg2 === 'object') {
|
|
||||||
for (const key of Object.keys(filterArg2)) {
|
|
||||||
if (key.startsWith('$')) {
|
|
||||||
convertedFilter[keyPathArg] = filterArg2;
|
|
||||||
return;
|
|
||||||
} else if (key.includes('.')) {
|
|
||||||
throw new Error('keys cannot contain dots');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(filterArg2)) {
|
|
||||||
convertFilterArgument(`${keyPathArg}.${key}`, filterArg2[key]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
convertedFilter[keyPathArg] = filterArg2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const key of Object.keys(filterArg)) {
|
|
||||||
convertFilterArgument(key, filterArg[key]);
|
|
||||||
}
|
|
||||||
const foundDocs = await (this as any).collection.find(convertedFilter);
|
|
||||||
const returnArray = [];
|
|
||||||
for (const item of foundDocs) {
|
|
||||||
const newInstance = new this();
|
|
||||||
(newInstance as any).creationStatus = 'db';
|
|
||||||
for (const key of Object.keys(item)) {
|
|
||||||
newInstance[key] = item[key];
|
|
||||||
}
|
|
||||||
returnArray.push(newInstance);
|
|
||||||
}
|
|
||||||
return returnArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getInstance<T>(
|
|
||||||
this: plugins.tsclass.typeFest.Class<T>,
|
|
||||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
|
|
||||||
): Promise<T> {
|
|
||||||
const result = await (this as any).getInstances(filterArg);
|
|
||||||
if (result && result.length > 0) {
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* saves this instance but not any connected items
|
* saves this instance but not any connected items
|
||||||
* may lead to data inconsistencies, but is faster
|
* may lead to data inconsistencies, but is faster
|
||||||
@ -134,6 +264,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements> {
|
|||||||
// tslint:disable-next-line: no-this-assignment
|
// tslint:disable-next-line: no-this-assignment
|
||||||
const self: any = this;
|
const self: any = this;
|
||||||
let dbResult: any;
|
let dbResult: any;
|
||||||
|
|
||||||
|
this._updatedAt = (new Date()).toISOString();
|
||||||
|
|
||||||
switch (this.creationStatus) {
|
switch (this.creationStatus) {
|
||||||
case 'db':
|
case 'db':
|
||||||
dbResult = await this.collection.update(self);
|
dbResult = await this.collection.update(self);
|
||||||
@ -159,9 +292,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements> {
|
|||||||
* also store any referenced objects to DB
|
* also store any referenced objects to DB
|
||||||
* better for data consistency
|
* better for data consistency
|
||||||
*/
|
*/
|
||||||
public saveDeep(savedMapArg: ObjectMap<SmartDataDbDoc<any, any>> = null) {
|
public saveDeep(savedMapArg: plugins.lik.ObjectMap<SmartDataDbDoc<any, any>> = null) {
|
||||||
if (!savedMapArg) {
|
if (!savedMapArg) {
|
||||||
savedMapArg = new ObjectMap<SmartDataDbDoc<any, any>>();
|
savedMapArg = new plugins.lik.ObjectMap<SmartDataDbDoc<any, any>>();
|
||||||
}
|
}
|
||||||
savedMapArg.add(this);
|
savedMapArg.add(this);
|
||||||
this.save();
|
this.save();
|
||||||
@ -173,12 +306,26 @@ export class SmartDataDbDoc<T extends TImplements, TImplements> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updates an object from db
|
||||||
|
*/
|
||||||
|
public async updateFromDb() {
|
||||||
|
const mongoDbNativeDoc = await this.collection.findOne(await this.createIdentifiableObject());
|
||||||
|
for (const key of Object.keys(mongoDbNativeDoc)) {
|
||||||
|
this[key] = mongoDbNativeDoc[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates a saveable object so the instance can be persisted as json in the database
|
* creates a saveable object so the instance can be persisted as json in the database
|
||||||
*/
|
*/
|
||||||
public async createSavableObject(): Promise<TImplements> {
|
public async createSavableObject(): Promise<TImplements> {
|
||||||
const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here
|
const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here
|
||||||
for (const propertyNameString of this.saveableProperties) {
|
const saveableProperties = [
|
||||||
|
...this.globalSaveableProperties,
|
||||||
|
...this.saveableProperties
|
||||||
|
]
|
||||||
|
for (const propertyNameString of saveableProperties) {
|
||||||
saveableObject[propertyNameString] = this[propertyNameString];
|
saveableObject[propertyNameString] = this[propertyNameString];
|
||||||
}
|
}
|
||||||
return saveableObject as TImplements;
|
return saveableObject as TImplements;
|
||||||
|
119
ts/smartdata.classes.easystore.ts
Normal file
119
ts/smartdata.classes.easystore.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
import { Collection } from './smartdata.classes.collection.js';
|
||||||
|
import { SmartdataDb } from './smartdata.classes.db.js';
|
||||||
|
import { SmartDataDbDoc, svDb, unI } from './smartdata.classes.doc.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EasyStore allows the storage of easy objects. It also allows easy sharing of the object between different instances
|
||||||
|
*/
|
||||||
|
export class EasyStore<T> {
|
||||||
|
// instance
|
||||||
|
public smartdataDbRef: SmartdataDb;
|
||||||
|
public nameId: string;
|
||||||
|
|
||||||
|
private easyStoreClass = (() => {
|
||||||
|
@Collection(() => this.smartdataDbRef)
|
||||||
|
class SmartdataEasyStore extends SmartDataDbDoc<SmartdataEasyStore, SmartdataEasyStore> {
|
||||||
|
@unI()
|
||||||
|
public nameId: string;
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public ephermal: {
|
||||||
|
activated: boolean;
|
||||||
|
timeout: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
lastEdit: number;
|
||||||
|
|
||||||
|
@svDb()
|
||||||
|
public data: Partial<T>;
|
||||||
|
}
|
||||||
|
return SmartdataEasyStore;
|
||||||
|
})();
|
||||||
|
|
||||||
|
constructor(nameIdArg: string, smnartdataDbRefArg: SmartdataDb) {
|
||||||
|
this.smartdataDbRef = smnartdataDbRefArg;
|
||||||
|
this.nameId = nameIdArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private easyStorePromise: Promise<InstanceType<typeof this.easyStoreClass>>;
|
||||||
|
private async getEasyStore(): Promise<InstanceType<typeof this.easyStoreClass>> {
|
||||||
|
if (this.easyStorePromise) {
|
||||||
|
return this.easyStorePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// first run from here
|
||||||
|
const deferred = plugins.smartpromise.defer<InstanceType<typeof this.easyStoreClass>>();
|
||||||
|
this.easyStorePromise = deferred.promise;
|
||||||
|
|
||||||
|
let easyStore = await this.easyStoreClass.getInstance({
|
||||||
|
nameId: this.nameId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!easyStore) {
|
||||||
|
easyStore = new this.easyStoreClass();
|
||||||
|
easyStore.nameId = this.nameId;
|
||||||
|
easyStore.data = {};
|
||||||
|
await easyStore.save();
|
||||||
|
}
|
||||||
|
deferred.resolve(easyStore);
|
||||||
|
return this.easyStorePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads all keyValue pairs at once and returns them
|
||||||
|
*/
|
||||||
|
public async readAll() {
|
||||||
|
const easyStore = await this.getEasyStore();
|
||||||
|
return easyStore.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reads a keyValueFile from disk
|
||||||
|
*/
|
||||||
|
public async readKey(keyArg: keyof T) {
|
||||||
|
const easyStore = await this.getEasyStore();
|
||||||
|
return easyStore.data[keyArg];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* writes a specific key to the keyValueStore
|
||||||
|
*/
|
||||||
|
public async writeKey<TKey extends keyof T>(keyArg: TKey, valueArg: T[TKey]) {
|
||||||
|
const easyStore = await this.getEasyStore();
|
||||||
|
easyStore.data[keyArg] = valueArg;
|
||||||
|
await easyStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteKey(keyArg: keyof T) {
|
||||||
|
const easyStore = await this.getEasyStore();
|
||||||
|
delete easyStore.data[keyArg];
|
||||||
|
await easyStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* writes all keyValue pairs in the object argument
|
||||||
|
*/
|
||||||
|
public async writeAll(keyValueObject: Partial<T>) {
|
||||||
|
const easyStore = await this.getEasyStore();
|
||||||
|
easyStore.data = { ...easyStore.data, ...keyValueObject };
|
||||||
|
await easyStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wipes a key value store from disk
|
||||||
|
*/
|
||||||
|
public async wipe() {
|
||||||
|
const easyStore = await this.getEasyStore();
|
||||||
|
easyStore.data = {};
|
||||||
|
await easyStore.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cleanUpEphermal() {
|
||||||
|
while (
|
||||||
|
(await this.smartdataDbRef.statusConnectedDeferred.promise) &&
|
||||||
|
this.smartdataDbRef.status === 'connected'
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
37
ts/smartdata.classes.watcher.ts
Normal file
37
ts/smartdata.classes.watcher.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { SmartDataDbDoc } from './smartdata.classes.doc.js';
|
||||||
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a wrapper for the native mongodb cursor. Exposes better
|
||||||
|
*/
|
||||||
|
export class SmartdataDbWatcher<T = any> {
|
||||||
|
// STATIC
|
||||||
|
public readyDeferred = plugins.smartpromise.defer();
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
private changeStream: plugins.mongodb.ChangeStream<T>;
|
||||||
|
|
||||||
|
public changeSubject = new plugins.smartrx.rxjs.Subject<T>();
|
||||||
|
constructor(
|
||||||
|
changeStreamArg: plugins.mongodb.ChangeStream<T>,
|
||||||
|
smartdataDbDocArg: typeof SmartDataDbDoc
|
||||||
|
) {
|
||||||
|
this.changeStream = changeStreamArg;
|
||||||
|
this.changeStream.on('change', async (item: any) => {
|
||||||
|
if (!item.fullDocument) {
|
||||||
|
this.changeSubject.next(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.changeSubject.next(
|
||||||
|
smartdataDbDocArg.createInstanceFromMongoDbNativeDoc(item.fullDocument) as any as T
|
||||||
|
);
|
||||||
|
});
|
||||||
|
plugins.smartdelay.delayFor(0).then(() => {
|
||||||
|
this.readyDeferred.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async close() {
|
||||||
|
await this.changeStream.close();
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
import * as plugins from './smartdata.plugins';
|
import * as plugins from './smartdata.plugins.js';
|
||||||
|
|
||||||
export const logger = new plugins.smartlog.ConsoleLog();
|
export const logger = new plugins.smartlog.ConsoleLog();
|
||||||
|
@ -4,11 +4,26 @@ import * as tsclass from '@tsclass/tsclass';
|
|||||||
export { tsclass };
|
export { tsclass };
|
||||||
|
|
||||||
// @pushrocks scope
|
// @pushrocks scope
|
||||||
import * as smartlog from '@pushrocks/smartlog';
|
import * as lik from '@push.rocks/lik';
|
||||||
import * as lodash from 'lodash';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
|
import * as smartstring from '@push.rocks/smartstring';
|
||||||
|
import * as smarttime from '@push.rocks/smarttime';
|
||||||
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
import * as mongodb from 'mongodb';
|
import * as mongodb from 'mongodb';
|
||||||
import * as smartq from '@pushrocks/smartpromise';
|
|
||||||
import * as smartstring from '@pushrocks/smartstring';
|
|
||||||
import * as smartunique from '@pushrocks/smartunique';
|
|
||||||
|
|
||||||
export { smartlog, lodash, smartq, mongodb, smartstring, smartunique };
|
export {
|
||||||
|
lik,
|
||||||
|
smartdelay,
|
||||||
|
smartpromise,
|
||||||
|
smartlog,
|
||||||
|
smartrx,
|
||||||
|
mongodb,
|
||||||
|
smartstring,
|
||||||
|
smarttime,
|
||||||
|
smartunique,
|
||||||
|
taskbuffer,
|
||||||
|
};
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "es2017",
|
"useDefineForClassFields": false,
|
||||||
"module": "commonjs"
|
"target": "ES2022",
|
||||||
}
|
"module": "NodeNext",
|
||||||
}
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist_*/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
17
tslint.json
17
tslint.json
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
|
||||||
"rules": {
|
|
||||||
"semicolon": [true, "always"],
|
|
||||||
"no-console": false,
|
|
||||||
"ordered-imports": false,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"member-ordering": {
|
|
||||||
"options":{
|
|
||||||
"order": [
|
|
||||||
"static-method"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultSeverity": "warning"
|
|
||||||
}
|
|
Reference in New Issue
Block a user