forked from tsai/budibase
164 changed files with 5 additions and 23199 deletions
@ -1,5 +1,3 @@ |
|||
import { common } from "../../../../core/src" |
|||
import { flow } from "lodash/fp"; |
|||
|
|||
export const pipe = common.$ |
|||
|
|||
export const events = common.eventsList |
|||
export const pipe = (arg, funcs) => flow(funcs)(arg) |
|||
|
|||
@ -1,12 +0,0 @@ |
|||
{ |
|||
"presets": ["@babel/preset-env"], |
|||
"sourceMaps": "inline", |
|||
"retainLines": true, |
|||
"plugins": [ |
|||
["@babel/plugin-transform-runtime", |
|||
{ |
|||
"regenerator": true |
|||
} |
|||
] |
|||
] |
|||
} |
|||
@ -1,43 +0,0 @@ |
|||
|
|||
# Logs |
|||
logs |
|||
*.log |
|||
|
|||
# Runtime data |
|||
pids |
|||
*.pid |
|||
*.seed |
|||
|
|||
# Directory for instrumented libs generated by jscoverage/JSCover |
|||
lib-cov |
|||
|
|||
# Coverage directory used by tools like istanbul |
|||
coverage |
|||
|
|||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
|||
.grunt |
|||
|
|||
# node-waf configuration |
|||
.lock-wscript |
|||
|
|||
# Compiled binary addons (http://nodejs.org/api/addons.html) |
|||
build/Release |
|||
.eslintcache |
|||
|
|||
# Dependency directory |
|||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git |
|||
node_modules |
|||
node_modules_ubuntu |
|||
node_modules_windows |
|||
|
|||
# OSX |
|||
.DS_Store |
|||
|
|||
# flow-typed |
|||
flow-typed/npm/* |
|||
!flow-typed/npm/module_vx.x.x.js |
|||
|
|||
|
|||
.idea |
|||
npm-debug.log.* |
|||
dist |
|||
@ -1,2 +0,0 @@ |
|||
* |
|||
!dist/* |
|||
@ -1,11 +0,0 @@ |
|||
sudo: required |
|||
|
|||
notifications: |
|||
slack: budibase:Nx2QNi9CP87Nn7ah2A4Qdzyy |
|||
|
|||
script: |
|||
- npm install |
|||
- npm install -g jest |
|||
- node node_modules/eslint/bin/eslint src/**/*.js |
|||
- jest |
|||
|
|||
@ -1,14 +0,0 @@ |
|||
{ |
|||
// Use IntelliSense to learn about possible attributes. |
|||
// Hover to view descriptions of existing attributes. |
|||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"type": "node", |
|||
"request": "launch", |
|||
"name": "Launch Program", |
|||
"program": "${workspaceFolder}\\index.js" |
|||
} |
|||
] |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
### Contributing to budibase-core |
|||
|
|||
* The contributors are listed in [AUTHORS.md](https://github.com/budibase/budibase-core/blob/master/AUTHORS.md) (add yourself). |
|||
|
|||
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/budibase-core/blob/master/LICENSE). |
|||
|
|||
* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. |
|||
Please read this if you are unfamiliar with it. |
|||
|
|||
* Please maintain the existing code style. |
|||
|
|||
* Please try to keep your commits small and focussed. |
|||
|
|||
* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. |
|||
|
|||
#### p.S... |
|||
|
|||
I am using contribution guidelines from the fantastic [ZeroMQ](https://github.com/zeromq) community. If you are interested why, it's because I believe in the ethos laid out by this community, and written about in depth in the book ["Social Architecture"](https://www.amazon.com/Social-Architecture-Building-line-Communities/dp/1533112452) by Pieter Hintjens. |
|||
|
|||
I am very much open to evolving this to suit our needs. |
|||
|
|||
Love from [Mike](https://github.com/mikebudi). |
|||
@ -1,373 +0,0 @@ |
|||
Mozilla Public License Version 2.0 |
|||
================================== |
|||
|
|||
1. Definitions |
|||
-------------- |
|||
|
|||
1.1. "Contributor" |
|||
means each individual or legal entity that creates, contributes to |
|||
the creation of, or owns Covered Software. |
|||
|
|||
1.2. "Contributor Version" |
|||
means the combination of the Contributions of others (if any) used |
|||
by a Contributor and that particular Contributor's Contribution. |
|||
|
|||
1.3. "Contribution" |
|||
means Covered Software of a particular Contributor. |
|||
|
|||
1.4. "Covered Software" |
|||
means Source Code Form to which the initial Contributor has attached |
|||
the notice in Exhibit A, the Executable Form of such Source Code |
|||
Form, and Modifications of such Source Code Form, in each case |
|||
including portions thereof. |
|||
|
|||
1.5. "Incompatible With Secondary Licenses" |
|||
means |
|||
|
|||
(a) that the initial Contributor has attached the notice described |
|||
in Exhibit B to the Covered Software; or |
|||
|
|||
(b) that the Covered Software was made available under the terms of |
|||
version 1.1 or earlier of the License, but not also under the |
|||
terms of a Secondary License. |
|||
|
|||
1.6. "Executable Form" |
|||
means any form of the work other than Source Code Form. |
|||
|
|||
1.7. "Larger Work" |
|||
means a work that combines Covered Software with other material, in |
|||
a separate file or files, that is not Covered Software. |
|||
|
|||
1.8. "License" |
|||
means this document. |
|||
|
|||
1.9. "Licensable" |
|||
means having the right to grant, to the maximum extent possible, |
|||
whether at the time of the initial grant or subsequently, any and |
|||
all of the rights conveyed by this License. |
|||
|
|||
1.10. "Modifications" |
|||
means any of the following: |
|||
|
|||
(a) any file in Source Code Form that results from an addition to, |
|||
deletion from, or modification of the contents of Covered |
|||
Software; or |
|||
|
|||
(b) any new file in Source Code Form that contains any Covered |
|||
Software. |
|||
|
|||
1.11. "Patent Claims" of a Contributor |
|||
means any patent claim(s), including without limitation, method, |
|||
process, and apparatus claims, in any patent Licensable by such |
|||
Contributor that would be infringed, but for the grant of the |
|||
License, by the making, using, selling, offering for sale, having |
|||
made, import, or transfer of either its Contributions or its |
|||
Contributor Version. |
|||
|
|||
1.12. "Secondary License" |
|||
means either the GNU General Public License, Version 2.0, the GNU |
|||
Lesser General Public License, Version 2.1, the GNU Affero General |
|||
Public License, Version 3.0, or any later versions of those |
|||
licenses. |
|||
|
|||
1.13. "Source Code Form" |
|||
means the form of the work preferred for making modifications. |
|||
|
|||
1.14. "You" (or "Your") |
|||
means an individual or a legal entity exercising rights under this |
|||
License. For legal entities, "You" includes any entity that |
|||
controls, is controlled by, or is under common control with You. For |
|||
purposes of this definition, "control" means (a) the power, direct |
|||
or indirect, to cause the direction or management of such entity, |
|||
whether by contract or otherwise, or (b) ownership of more than |
|||
fifty percent (50%) of the outstanding shares or beneficial |
|||
ownership of such entity. |
|||
|
|||
2. License Grants and Conditions |
|||
-------------------------------- |
|||
|
|||
2.1. Grants |
|||
|
|||
Each Contributor hereby grants You a world-wide, royalty-free, |
|||
non-exclusive license: |
|||
|
|||
(a) under intellectual property rights (other than patent or trademark) |
|||
Licensable by such Contributor to use, reproduce, make available, |
|||
modify, display, perform, distribute, and otherwise exploit its |
|||
Contributions, either on an unmodified basis, with Modifications, or |
|||
as part of a Larger Work; and |
|||
|
|||
(b) under Patent Claims of such Contributor to make, use, sell, offer |
|||
for sale, have made, import, and otherwise transfer either its |
|||
Contributions or its Contributor Version. |
|||
|
|||
2.2. Effective Date |
|||
|
|||
The licenses granted in Section 2.1 with respect to any Contribution |
|||
become effective for each Contribution on the date the Contributor first |
|||
distributes such Contribution. |
|||
|
|||
2.3. Limitations on Grant Scope |
|||
|
|||
The licenses granted in this Section 2 are the only rights granted under |
|||
this License. No additional rights or licenses will be implied from the |
|||
distribution or licensing of Covered Software under this License. |
|||
Notwithstanding Section 2.1(b) above, no patent license is granted by a |
|||
Contributor: |
|||
|
|||
(a) for any code that a Contributor has removed from Covered Software; |
|||
or |
|||
|
|||
(b) for infringements caused by: (i) Your and any other third party's |
|||
modifications of Covered Software, or (ii) the combination of its |
|||
Contributions with other software (except as part of its Contributor |
|||
Version); or |
|||
|
|||
(c) under Patent Claims infringed by Covered Software in the absence of |
|||
its Contributions. |
|||
|
|||
This License does not grant any rights in the trademarks, service marks, |
|||
or logos of any Contributor (except as may be necessary to comply with |
|||
the notice requirements in Section 3.4). |
|||
|
|||
2.4. Subsequent Licenses |
|||
|
|||
No Contributor makes additional grants as a result of Your choice to |
|||
distribute the Covered Software under a subsequent version of this |
|||
License (see Section 10.2) or under the terms of a Secondary License (if |
|||
permitted under the terms of Section 3.3). |
|||
|
|||
2.5. Representation |
|||
|
|||
Each Contributor represents that the Contributor believes its |
|||
Contributions are its original creation(s) or it has sufficient rights |
|||
to grant the rights to its Contributions conveyed by this License. |
|||
|
|||
2.6. Fair Use |
|||
|
|||
This License is not intended to limit any rights You have under |
|||
applicable copyright doctrines of fair use, fair dealing, or other |
|||
equivalents. |
|||
|
|||
2.7. Conditions |
|||
|
|||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted |
|||
in Section 2.1. |
|||
|
|||
3. Responsibilities |
|||
------------------- |
|||
|
|||
3.1. Distribution of Source Form |
|||
|
|||
All distribution of Covered Software in Source Code Form, including any |
|||
Modifications that You create or to which You contribute, must be under |
|||
the terms of this License. You must inform recipients that the Source |
|||
Code Form of the Covered Software is governed by the terms of this |
|||
License, and how they can obtain a copy of this License. You may not |
|||
attempt to alter or restrict the recipients' rights in the Source Code |
|||
Form. |
|||
|
|||
3.2. Distribution of Executable Form |
|||
|
|||
If You distribute Covered Software in Executable Form then: |
|||
|
|||
(a) such Covered Software must also be made available in Source Code |
|||
Form, as described in Section 3.1, and You must inform recipients of |
|||
the Executable Form how they can obtain a copy of such Source Code |
|||
Form by reasonable means in a timely manner, at a charge no more |
|||
than the cost of distribution to the recipient; and |
|||
|
|||
(b) You may distribute such Executable Form under the terms of this |
|||
License, or sublicense it under different terms, provided that the |
|||
license for the Executable Form does not attempt to limit or alter |
|||
the recipients' rights in the Source Code Form under this License. |
|||
|
|||
3.3. Distribution of a Larger Work |
|||
|
|||
You may create and distribute a Larger Work under terms of Your choice, |
|||
provided that You also comply with the requirements of this License for |
|||
the Covered Software. If the Larger Work is a combination of Covered |
|||
Software with a work governed by one or more Secondary Licenses, and the |
|||
Covered Software is not Incompatible With Secondary Licenses, this |
|||
License permits You to additionally distribute such Covered Software |
|||
under the terms of such Secondary License(s), so that the recipient of |
|||
the Larger Work may, at their option, further distribute the Covered |
|||
Software under the terms of either this License or such Secondary |
|||
License(s). |
|||
|
|||
3.4. Notices |
|||
|
|||
You may not remove or alter the substance of any license notices |
|||
(including copyright notices, patent notices, disclaimers of warranty, |
|||
or limitations of liability) contained within the Source Code Form of |
|||
the Covered Software, except that You may alter any license notices to |
|||
the extent required to remedy known factual inaccuracies. |
|||
|
|||
3.5. Application of Additional Terms |
|||
|
|||
You may choose to offer, and to charge a fee for, warranty, support, |
|||
indemnity or liability obligations to one or more recipients of Covered |
|||
Software. However, You may do so only on Your own behalf, and not on |
|||
behalf of any Contributor. You must make it absolutely clear that any |
|||
such warranty, support, indemnity, or liability obligation is offered by |
|||
You alone, and You hereby agree to indemnify every Contributor for any |
|||
liability incurred by such Contributor as a result of warranty, support, |
|||
indemnity or liability terms You offer. You may include additional |
|||
disclaimers of warranty and limitations of liability specific to any |
|||
jurisdiction. |
|||
|
|||
4. Inability to Comply Due to Statute or Regulation |
|||
--------------------------------------------------- |
|||
|
|||
If it is impossible for You to comply with any of the terms of this |
|||
License with respect to some or all of the Covered Software due to |
|||
statute, judicial order, or regulation then You must: (a) comply with |
|||
the terms of this License to the maximum extent possible; and (b) |
|||
describe the limitations and the code they affect. Such description must |
|||
be placed in a text file included with all distributions of the Covered |
|||
Software under this License. Except to the extent prohibited by statute |
|||
or regulation, such description must be sufficiently detailed for a |
|||
recipient of ordinary skill to be able to understand it. |
|||
|
|||
5. Termination |
|||
-------------- |
|||
|
|||
5.1. The rights granted under this License will terminate automatically |
|||
if You fail to comply with any of its terms. However, if You become |
|||
compliant, then the rights granted under this License from a particular |
|||
Contributor are reinstated (a) provisionally, unless and until such |
|||
Contributor explicitly and finally terminates Your grants, and (b) on an |
|||
ongoing basis, if such Contributor fails to notify You of the |
|||
non-compliance by some reasonable means prior to 60 days after You have |
|||
come back into compliance. Moreover, Your grants from a particular |
|||
Contributor are reinstated on an ongoing basis if such Contributor |
|||
notifies You of the non-compliance by some reasonable means, this is the |
|||
first time You have received notice of non-compliance with this License |
|||
from such Contributor, and You become compliant prior to 30 days after |
|||
Your receipt of the notice. |
|||
|
|||
5.2. If You initiate litigation against any entity by asserting a patent |
|||
infringement claim (excluding declaratory judgment actions, |
|||
counter-claims, and cross-claims) alleging that a Contributor Version |
|||
directly or indirectly infringes any patent, then the rights granted to |
|||
You by any and all Contributors for the Covered Software under Section |
|||
2.1 of this License shall terminate. |
|||
|
|||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all |
|||
end user license agreements (excluding distributors and resellers) which |
|||
have been validly granted by You or Your distributors under this License |
|||
prior to termination shall survive termination. |
|||
|
|||
************************************************************************ |
|||
* * |
|||
* 6. Disclaimer of Warranty * |
|||
* ------------------------- * |
|||
* * |
|||
* Covered Software is provided under this License on an "as is" * |
|||
* basis, without warranty of any kind, either expressed, implied, or * |
|||
* statutory, including, without limitation, warranties that the * |
|||
* Covered Software is free of defects, merchantable, fit for a * |
|||
* particular purpose or non-infringing. The entire risk as to the * |
|||
* quality and performance of the Covered Software is with You. * |
|||
* Should any Covered Software prove defective in any respect, You * |
|||
* (not any Contributor) assume the cost of any necessary servicing, * |
|||
* repair, or correction. This disclaimer of warranty constitutes an * |
|||
* essential part of this License. No use of any Covered Software is * |
|||
* authorized under this License except under this disclaimer. * |
|||
* * |
|||
************************************************************************ |
|||
|
|||
************************************************************************ |
|||
* * |
|||
* 7. Limitation of Liability * |
|||
* -------------------------- * |
|||
* * |
|||
* Under no circumstances and under no legal theory, whether tort * |
|||
* (including negligence), contract, or otherwise, shall any * |
|||
* Contributor, or anyone who distributes Covered Software as * |
|||
* permitted above, be liable to You for any direct, indirect, * |
|||
* special, incidental, or consequential damages of any character * |
|||
* including, without limitation, damages for lost profits, loss of * |
|||
* goodwill, work stoppage, computer failure or malfunction, or any * |
|||
* and all other commercial damages or losses, even if such party * |
|||
* shall have been informed of the possibility of such damages. This * |
|||
* limitation of liability shall not apply to liability for death or * |
|||
* personal injury resulting from such party's negligence to the * |
|||
* extent applicable law prohibits such limitation. Some * |
|||
* jurisdictions do not allow the exclusion or limitation of * |
|||
* incidental or consequential damages, so this exclusion and * |
|||
* limitation may not apply to You. * |
|||
* * |
|||
************************************************************************ |
|||
|
|||
8. Litigation |
|||
------------- |
|||
|
|||
Any litigation relating to this License may be brought only in the |
|||
courts of a jurisdiction where the defendant maintains its principal |
|||
place of business and such litigation shall be governed by laws of that |
|||
jurisdiction, without reference to its conflict-of-law provisions. |
|||
Nothing in this Section shall prevent a party's ability to bring |
|||
cross-claims or counter-claims. |
|||
|
|||
9. Miscellaneous |
|||
---------------- |
|||
|
|||
This License represents the complete agreement concerning the subject |
|||
matter hereof. If any provision of this License is held to be |
|||
unenforceable, such provision shall be reformed only to the extent |
|||
necessary to make it enforceable. Any law or regulation which provides |
|||
that the language of a contract shall be construed against the drafter |
|||
shall not be used to construe this License against a Contributor. |
|||
|
|||
10. Versions of the License |
|||
--------------------------- |
|||
|
|||
10.1. New Versions |
|||
|
|||
Mozilla Foundation is the license steward. Except as provided in Section |
|||
10.3, no one other than the license steward has the right to modify or |
|||
publish new versions of this License. Each version will be given a |
|||
distinguishing version number. |
|||
|
|||
10.2. Effect of New Versions |
|||
|
|||
You may distribute the Covered Software under the terms of the version |
|||
of the License under which You originally received the Covered Software, |
|||
or under the terms of any subsequent version published by the license |
|||
steward. |
|||
|
|||
10.3. Modified Versions |
|||
|
|||
If you create software not governed by this License, and you want to |
|||
create a new license for such software, you may create and use a |
|||
modified version of this License if you rename the license and remove |
|||
any references to the name of the license steward (except to note that |
|||
such modified license differs from this License). |
|||
|
|||
10.4. Distributing Source Code Form that is Incompatible With Secondary |
|||
Licenses |
|||
|
|||
If You choose to distribute Source Code Form that is Incompatible With |
|||
Secondary Licenses under the terms of this version of the License, the |
|||
notice described in Exhibit B of this License must be attached. |
|||
|
|||
Exhibit A - Source Code Form License Notice |
|||
------------------------------------------- |
|||
|
|||
This Source Code Form is subject to the terms of the Mozilla Public |
|||
License, v. 2.0. If a copy of the MPL was not distributed with this |
|||
file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|||
|
|||
If it is not possible or desirable to put the notice in a particular |
|||
file, then You may include the notice in a location (such as a LICENSE |
|||
file in a relevant directory) where a recipient would be likely to look |
|||
for such a notice. |
|||
|
|||
You may add additional accurate notices of copyright ownership. |
|||
|
|||
Exhibit B - "Incompatible With Secondary Licenses" Notice |
|||
--------------------------------------------------------- |
|||
|
|||
This Source Code Form is "Incompatible With Secondary Licenses", as |
|||
defined by the Mozilla Public License, v. 2.0. |
|||
@ -1,83 +0,0 @@ |
|||
{ |
|||
"name": "@budibase/core", |
|||
"version": "0.0.32", |
|||
"description": "core javascript library for budibase", |
|||
"main": "dist/budibase-core.umd.js", |
|||
"module": "dist/budibase-core.esm.js", |
|||
"files": [ |
|||
"dist/**", |
|||
"!dist/node_modules" |
|||
], |
|||
"directories": { |
|||
"test": "test" |
|||
}, |
|||
"scripts": { |
|||
"test": "jest", |
|||
"test:watch": "jest --watch", |
|||
"build": "rollup -c rollup.config.js" |
|||
}, |
|||
"keywords": [ |
|||
"budibase" |
|||
], |
|||
"author": "Michael Shanks", |
|||
"license": "MPL-2.0", |
|||
"jest": { |
|||
"globals": { |
|||
"GLOBALS": { |
|||
"client": "web" |
|||
} |
|||
}, |
|||
"testURL": "http://jest-breaks-if-this-does-not-exist", |
|||
"moduleNameMapper": { |
|||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js", |
|||
"\\.(css|less|sass|scss)$": "identity-obj-proxy" |
|||
}, |
|||
"moduleFileExtensions": [ |
|||
"js" |
|||
], |
|||
"moduleDirectories": [ |
|||
"node_modules" |
|||
], |
|||
"transform": { |
|||
"^.+\\.js$": "babel-jest" |
|||
}, |
|||
"transformIgnorePatterns": [ |
|||
"/node_modules/(?!svelte).+\\.js$" |
|||
] |
|||
}, |
|||
"devDependencies": { |
|||
"@babel/cli": "^7.4.4", |
|||
"@babel/core": "^7.4.5", |
|||
"@babel/plugin-transform-runtime": "^7.4.4", |
|||
"@babel/preset-env": "^7.4.5", |
|||
"@babel/runtime": "^7.4.5", |
|||
"babel-jest": "^23.6.0", |
|||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", |
|||
"cross-env": "^5.1.4", |
|||
"jest": "^24.8.0", |
|||
"readable-stream": "^3.1.1", |
|||
"regenerator-runtime": "^0.11.1", |
|||
"rimraf": "^2.6.2", |
|||
"rollup": "^1.12.0", |
|||
"rollup-plugin-commonjs": "^10.0.0", |
|||
"rollup-plugin-node-builtins": "^2.1.2", |
|||
"rollup-plugin-node-globals": "^1.4.0", |
|||
"rollup-plugin-node-resolve": "^5.0.0" |
|||
}, |
|||
"dependencies": { |
|||
"@nx-js/compiler-util": "^2.0.0", |
|||
"bcryptjs": "^2.4.3", |
|||
"date-fns": "^1.29.0", |
|||
"lodash": "^4.17.13", |
|||
"lunr": "^2.3.5", |
|||
"nano": "^8.2.2", |
|||
"safe-buffer": "^5.1.2", |
|||
"shortid": "^2.2.8" |
|||
}, |
|||
"devEngines": { |
|||
"node": ">=7.x", |
|||
"npm": ">=4.x", |
|||
"yarn": ">=0.21.3" |
|||
}, |
|||
"gitHead": "b1f4f90927d9e494e513220ef060af28d2d42455" |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
## Getting Started |
|||
|
|||
Install packages: |
|||
|
|||
`npm install` |
|||
|
|||
Next, run the tests. Install jest, globally: |
|||
|
|||
`npm install -g jest` |
|||
|
|||
And finally, run |
|||
|
|||
`jest` |
|||
|
|||
## Documentation |
|||
|
|||
A work in progress, lives here: https://github.com/Budibase/docs/blob/master/budibase-core.md |
|||
|
|||
|
|||
|
|||
|
|||
@ -1,153 +0,0 @@ |
|||
import builtins from "rollup-plugin-node-builtins" |
|||
import resolve from "rollup-plugin-node-resolve" |
|||
import commonjs from "rollup-plugin-commonjs" |
|||
import nodeglobals from "rollup-plugin-node-globals" |
|||
|
|||
const lodash_fp_exports = [ |
|||
"union", |
|||
"reduce", |
|||
"isUndefined", |
|||
"cloneDeep", |
|||
"split", |
|||
"some", |
|||
"map", |
|||
"filter", |
|||
"isEmpty", |
|||
"countBy", |
|||
"includes", |
|||
"last", |
|||
"find", |
|||
"constant", |
|||
"take", |
|||
"first", |
|||
"intersection", |
|||
"mapValues", |
|||
"isNull", |
|||
"has", |
|||
"isNumber", |
|||
"isString", |
|||
"isBoolean", |
|||
"isDate", |
|||
"isArray", |
|||
"isObject", |
|||
"clone", |
|||
"values", |
|||
"keyBy", |
|||
"keys", |
|||
"orderBy", |
|||
"concat", |
|||
"reverse", |
|||
"difference", |
|||
"merge", |
|||
"flatten", |
|||
"each", |
|||
"pull", |
|||
"join", |
|||
"defaultCase", |
|||
"uniqBy", |
|||
"every", |
|||
"uniqWith", |
|||
"isFunction", |
|||
"groupBy", |
|||
"differenceBy", |
|||
"intersectionBy", |
|||
"isEqual", |
|||
"max", |
|||
] |
|||
|
|||
const lodash_exports = [ |
|||
"toNumber", |
|||
"flow", |
|||
"isArray", |
|||
"join", |
|||
"replace", |
|||
"trim", |
|||
"dropRight", |
|||
"takeRight", |
|||
"head", |
|||
"isUndefined", |
|||
"isNull", |
|||
"isNaN", |
|||
"reduce", |
|||
"isEmpty", |
|||
"constant", |
|||
"tail", |
|||
"includes", |
|||
"startsWith", |
|||
"findIndex", |
|||
"isInteger", |
|||
"isDate", |
|||
"isString", |
|||
"split", |
|||
"clone", |
|||
"keys", |
|||
"isFunction", |
|||
"merge", |
|||
"has", |
|||
"isBoolean", |
|||
"isNumber", |
|||
"isObjectLike", |
|||
"assign", |
|||
"some", |
|||
"each", |
|||
"find", |
|||
"orderBy", |
|||
"union", |
|||
"cloneDeep", |
|||
] |
|||
|
|||
const globals = { |
|||
"lodash/fp": "fp", |
|||
lodash: "_", |
|||
lunr: "lunr", |
|||
"safe-buffer": "safe_buffer", |
|||
shortid: "shortid", |
|||
"@nx-js/compiler-util": "compiler_util", |
|||
} |
|||
|
|||
export default { |
|||
input: "src/index.js", |
|||
output: [ |
|||
{ |
|||
file: "dist/budibase-core.cjs.js", |
|||
format: "cjs", |
|||
sourcemap: "inline", |
|||
}, |
|||
{ |
|||
file: "dist/budibase-core.esm.mjs", |
|||
format: "esm", |
|||
sourcemap: "inline", |
|||
}, |
|||
{ |
|||
file: "dist/budibase-core.umd.js", |
|||
format: "umd", |
|||
name: "@budibase/core", |
|||
sourcemap: "inline", |
|||
globals, |
|||
}, |
|||
], |
|||
plugins: [ |
|||
nodeglobals(), |
|||
builtins(), |
|||
resolve({ |
|||
preferBuiltins: true, |
|||
}), |
|||
commonjs({ |
|||
namedExports: { |
|||
"lodash/fp": lodash_fp_exports, |
|||
lodash: lodash_exports, |
|||
shortid: ["generate"], |
|||
}, |
|||
}), |
|||
], |
|||
external: [ |
|||
"lodash", |
|||
"lodash/fp", |
|||
"date-fns", |
|||
"lunr", |
|||
"safe-buffer", |
|||
"shortid", |
|||
"@nx-js/compiler-util", |
|||
"bcryptjs", |
|||
], |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
import { has } from "lodash" |
|||
import { ConflictError } from "../common/errors" |
|||
|
|||
export const createBehaviourSources = () => { |
|||
const sources = {} |
|||
const register = (name, funcsObj) => { |
|||
if (has(sources, name)) { |
|||
throw new ConflictError(`Source '${name}' already exists`) |
|||
} |
|||
|
|||
sources[name] = funcsObj |
|||
} |
|||
sources.register = register |
|||
return sources |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
import { permission } from "../authApi/permissions" |
|||
import { apiWrapperSync } from "../common/apiWrapper" |
|||
import { events } from "../common/events" |
|||
|
|||
export const executeAction = app => (actionName, options) => { |
|||
apiWrapperSync( |
|||
app, |
|||
events.actionsApi.execute, |
|||
permission.executeAction.isAuthorized(actionName), |
|||
{ actionName, options }, |
|||
app.actions[actionName], |
|||
options |
|||
) |
|||
} |
|||
|
|||
export const _executeAction = (behaviourSources, action, options) => |
|||
behaviourSources[action.behaviourSource][action.behaviourName](options) |
|||
@ -1,7 +0,0 @@ |
|||
import { executeAction } from "./execute" |
|||
|
|||
export const getActionsApi = app => ({ |
|||
execute: executeAction(app), |
|||
}) |
|||
|
|||
export default getActionsApi |
|||
@ -1,103 +0,0 @@ |
|||
import { |
|||
isFunction, |
|||
filter, |
|||
map, |
|||
uniqBy, |
|||
keys, |
|||
difference, |
|||
join, |
|||
reduce, |
|||
find, |
|||
} from "lodash/fp" |
|||
import { compileCode } from "../common/compileCode" |
|||
import { $ } from "../common" |
|||
import { _executeAction } from "./execute" |
|||
import { BadRequestError, NotFoundError } from "../common/errors" |
|||
|
|||
export const initialiseActions = ( |
|||
subscribe, |
|||
behaviourSources, |
|||
actions, |
|||
triggers, |
|||
apis |
|||
) => { |
|||
validateSources(behaviourSources, actions) |
|||
subscribeTriggers(subscribe, behaviourSources, actions, triggers, apis) |
|||
return createActionsCollection(behaviourSources, actions) |
|||
} |
|||
|
|||
const createActionsCollection = (behaviourSources, actions) => |
|||
$(actions, [ |
|||
reduce((all, a) => { |
|||
all[a.name] = opts => _executeAction(behaviourSources, a, opts) |
|||
return all |
|||
}, {}), |
|||
]) |
|||
|
|||
const subscribeTriggers = ( |
|||
subscribe, |
|||
behaviourSources, |
|||
actions, |
|||
triggers, |
|||
apis |
|||
) => { |
|||
const createOptions = (optionsCreator, eventContext) => { |
|||
if (!optionsCreator) return {} |
|||
const create = compileCode(optionsCreator) |
|||
return create({ context: eventContext, apis }) |
|||
} |
|||
|
|||
const shouldRunTrigger = (trigger, eventContext) => { |
|||
if (!trigger.condition) return true |
|||
const shouldRun = compileCode(trigger.condition) |
|||
return shouldRun({ context: eventContext }) |
|||
} |
|||
|
|||
for (let trig of triggers) { |
|||
subscribe(trig.eventName, async (ev, ctx) => { |
|||
if (shouldRunTrigger(trig, ctx)) { |
|||
await _executeAction( |
|||
behaviourSources, |
|||
find(a => a.name === trig.actionName)(actions), |
|||
createOptions(trig.optionsCreator, ctx) |
|||
) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const validateSources = (behaviourSources, actions) => { |
|||
const declaredSources = $(actions, [ |
|||
uniqBy(a => a.behaviourSource), |
|||
map(a => a.behaviourSource), |
|||
]) |
|||
|
|||
const suppliedSources = keys(behaviourSources) |
|||
|
|||
const missingSources = difference(declaredSources, suppliedSources) |
|||
|
|||
if (missingSources.length > 0) { |
|||
throw new BadRequestError( |
|||
`Declared behaviour sources are not supplied: ${join( |
|||
", ", |
|||
missingSources |
|||
)}` |
|||
) |
|||
} |
|||
|
|||
const missingBehaviours = $(actions, [ |
|||
filter( |
|||
a => !isFunction(behaviourSources[a.behaviourSource][a.behaviourName]) |
|||
), |
|||
map(a => `Action: ${a.name} : ${a.behaviourSource}.${a.behaviourName}`), |
|||
]) |
|||
|
|||
if (missingBehaviours.length > 0) { |
|||
throw new NotFoundError( |
|||
`Missing behaviours: could not find behaviour functions: ${join( |
|||
", ", |
|||
missingBehaviours |
|||
)}` |
|||
) |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
import { setCleanupFunc } from "../transactions/setCleanupFunc" |
|||
|
|||
export const cloneApp = (app, mergeWith) => { |
|||
const newApp = { ...app } |
|||
Object.assign(newApp, mergeWith) |
|||
setCleanupFunc(newApp) |
|||
return newApp |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
import { has } from "lodash/fp" |
|||
|
|||
const publish = handlers => async (eventName, context = {}) => { |
|||
if (!has(eventName)(handlers)) return |
|||
|
|||
for (const handler of handlers[eventName]) { |
|||
await handler(eventName, context) |
|||
} |
|||
} |
|||
|
|||
const subscribe = handlers => (eventName, handler) => { |
|||
if (!has(eventName)(handlers)) { |
|||
handlers[eventName] = [] |
|||
} |
|||
handlers[eventName].push(handler) |
|||
} |
|||
|
|||
export const createEventAggregator = () => { |
|||
const handlers = {} |
|||
const eventAggregator = { |
|||
publish: publish(handlers), |
|||
subscribe: subscribe(handlers), |
|||
} |
|||
return eventAggregator |
|||
} |
|||
|
|||
export default createEventAggregator |
|||
@ -1,13 +0,0 @@ |
|||
export const setupDatastore = datastore => { |
|||
datastore.loadJson = datastore.loadFile |
|||
datastore.createJson = datastore.createFile |
|||
datastore.updateJson = datastore.updateFile |
|||
if (datastore.createEmptyDb) { |
|||
delete datastore.createEmptyDb |
|||
} |
|||
return datastore |
|||
} |
|||
|
|||
export { createEventAggregator } from "./eventAggregator" |
|||
|
|||
export default setupDatastore |
|||
@ -1,55 +0,0 @@ |
|||
import { filter } from "lodash/fp" |
|||
import { configFolder, appDefinitionFile, $ } from "../common" |
|||
import { TRANSACTIONS_FOLDER } from "../transactions/transactionsCommon" |
|||
import { |
|||
AUTH_FOLDER, |
|||
USERS_LIST_FILE, |
|||
ACCESS_LEVELS_FILE, |
|||
} from "../authApi/authCommon" |
|||
import { initialiseRootCollections } from "../collectionApi/initialise" |
|||
import { initialiseIndex } from "../indexing/initialiseIndex" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
isGlobalIndex, |
|||
isSingleRecord, |
|||
} from "../templateApi/hierarchy" |
|||
import { _getNew } from "../recordApi/getNew" |
|||
import { _save } from "../recordApi/save" |
|||
|
|||
export const initialiseData = async ( |
|||
datastore, |
|||
applicationDefinition, |
|||
accessLevels |
|||
) => { |
|||
if (!(await datastore.exists(appDefinitionFile))) |
|||
await datastore.createJson(appDefinitionFile, applicationDefinition) |
|||
|
|||
if (!(await datastore.exists(USERS_LIST_FILE))) |
|||
await datastore.createJson(USERS_LIST_FILE, []) |
|||
|
|||
if (!(await datastore.exists(ACCESS_LEVELS_FILE))) |
|||
await datastore.createJson( |
|||
ACCESS_LEVELS_FILE, |
|||
accessLevels ? accessLevels : { version: 0, levels: [] } |
|||
) |
|||
|
|||
await initialiseRootSingleRecords(datastore, applicationDefinition.hierarchy) |
|||
} |
|||
|
|||
const initialiseRootSingleRecords = async (datastore, hierarchy) => { |
|||
const app = { |
|||
publish: () => {}, |
|||
cleanupTransactions: () => {}, |
|||
datastore, |
|||
hierarchy, |
|||
} |
|||
|
|||
const flathierarchy = getFlattenedHierarchy(hierarchy) |
|||
const singleRecords = $(flathierarchy, [filter(isSingleRecord)]) |
|||
|
|||
for (let record of singleRecords) { |
|||
const result = _getNew(record, "") |
|||
result.key = record.nodeKey() |
|||
await _save(app, result) |
|||
} |
|||
} |
|||
@ -1,53 +0,0 @@ |
|||
import { clone, find, split } from "lodash/fp" |
|||
import { joinKey, $ } from "../common" |
|||
// 5 minutes
|
|||
export const tempCodeExpiryLength = 5 * 60 * 1000 |
|||
|
|||
export const AUTH_FOLDER = "/.auth" |
|||
export const USERS_LIST_FILE = joinKey(AUTH_FOLDER, "users.json") |
|||
export const userAuthFile = username => |
|||
joinKey(AUTH_FOLDER, `auth_${username}.json`) |
|||
export const USERS_LOCK_FILE = joinKey(AUTH_FOLDER, "users_lock") |
|||
export const ACCESS_LEVELS_FILE = joinKey(AUTH_FOLDER, "access_levels.json") |
|||
export const ACCESS_LEVELS_LOCK_FILE = joinKey( |
|||
AUTH_FOLDER, |
|||
"access_levels_lock" |
|||
) |
|||
|
|||
export const permissionTypes = { |
|||
CREATE_RECORD: "create record", |
|||
UPDATE_RECORD: "update record", |
|||
READ_RECORD: "read record", |
|||
DELETE_RECORD: "delete record", |
|||
READ_INDEX: "read index", |
|||
MANAGE_INDEX: "manage index", |
|||
MANAGE_COLLECTION: "manage collection", |
|||
WRITE_TEMPLATES: "write templates", |
|||
CREATE_USER: "create user", |
|||
SET_PASSWORD: "set password", |
|||
CREATE_TEMPORARY_ACCESS: "create temporary access", |
|||
ENABLE_DISABLE_USER: "enable or disable user", |
|||
WRITE_ACCESS_LEVELS: "write access levels", |
|||
LIST_USERS: "list users", |
|||
LIST_ACCESS_LEVELS: "list access levels", |
|||
EXECUTE_ACTION: "execute action", |
|||
SET_USER_ACCESS_LEVELS: "set user access levels", |
|||
} |
|||
|
|||
export const getUserByName = (users, name) => |
|||
$(users, [find(u => u.name.toLowerCase() === name.toLowerCase())]) |
|||
|
|||
export const stripUserOfSensitiveStuff = user => { |
|||
const stripped = clone(user) |
|||
delete stripped.tempCode |
|||
return stripped |
|||
} |
|||
|
|||
export const parseTemporaryCode = fullCode => |
|||
$(fullCode, [ |
|||
split(":"), |
|||
parts => ({ |
|||
id: parts[1], |
|||
code: parts[2], |
|||
}), |
|||
]) |
|||
@ -1,118 +0,0 @@ |
|||
import { find, filter, some, map, flatten } from "lodash/fp" |
|||
import { generate } from "shortid" |
|||
import { _getUsers } from "./getUsers" |
|||
import { getUserByName, userAuthFile, parseTemporaryCode } from "./authCommon" |
|||
import { _loadAccessLevels } from "./loadAccessLevels" |
|||
import { isNothingOrEmpty, $, apiWrapper, events } from "../common" |
|||
import { alwaysAuthorized } from "./permissions" |
|||
|
|||
const dummyHash = |
|||
"$argon2i$v=19$m=4096,t=3,p=1$UZRo409UYBGjHJS3CV6Uxw$rU84qUqPeORFzKYmYY0ceBLDaPO+JWSH4PfNiKXfIKk" |
|||
|
|||
export const authenticate = app => async (username, password) => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.authenticate, |
|||
alwaysAuthorized, |
|||
{ username, password }, |
|||
_authenticate, |
|||
app, |
|||
username, |
|||
password |
|||
) |
|||
|
|||
export const _authenticate = async (app, username, password) => { |
|||
if (isNothingOrEmpty(username) || isNothingOrEmpty(password)) { |
|||
return null |
|||
} |
|||
|
|||
const allUsers = await _getUsers(app) |
|||
let user = getUserByName(allUsers, username) |
|||
|
|||
const notAUser = "not-a-user" |
|||
// continue with non-user - so time to verify remains consistent
|
|||
// with verification of a valid user
|
|||
if (!user || !user.enabled) { |
|||
user = notAUser |
|||
} |
|||
|
|||
let userAuth |
|||
try { |
|||
userAuth = await app.datastore.loadJson(userAuthFile(username)) |
|||
} catch (_) { |
|||
userAuth = { accessLevels: [], passwordHash: dummyHash } |
|||
} |
|||
|
|||
const permissions = await buildUserPermissions(app, user.accessLevels) |
|||
|
|||
const verified = app.crypto.verify(userAuth.passwordHash, password) |
|||
|
|||
if (user === notAUser) { |
|||
return null |
|||
} |
|||
|
|||
return verified |
|||
? { |
|||
...user, |
|||
permissions, |
|||
temp: false, |
|||
isUser: true, |
|||
} |
|||
: null |
|||
} |
|||
|
|||
export const authenticateTemporaryAccess = app => async tempAccessCode => { |
|||
if (isNothingOrEmpty(tempAccessCode)) { |
|||
return null |
|||
} |
|||
|
|||
const temp = parseTemporaryCode(tempAccessCode) |
|||
let user = $(await _getUsers(app), [ |
|||
find(u => u.temporaryAccessId === temp.id), |
|||
]) |
|||
|
|||
const notAUser = "not-a-user" |
|||
if (!user || !user.enabled) { |
|||
user = notAUser |
|||
} |
|||
|
|||
let userAuth |
|||
try { |
|||
userAuth = await app.datastore.loadJson(userAuthFile(user.name)) |
|||
} catch (e) { |
|||
userAuth = { |
|||
temporaryAccessHash: dummyHash, |
|||
temporaryAccessExpiryEpoch: (await app.getEpochTime()) + 10000, |
|||
} |
|||
} |
|||
|
|||
if (userAuth.temporaryAccessExpiryEpoch < (await app.getEpochTime())) { |
|||
user = notAUser |
|||
} |
|||
|
|||
const tempCode = !temp.code ? generate() : temp.code |
|||
const verified = app.crypto.verify(userAuth.temporaryAccessHash, tempCode) |
|||
|
|||
if (user === notAUser) { |
|||
return null |
|||
} |
|||
|
|||
return verified |
|||
? { |
|||
...user, |
|||
permissions: [], |
|||
temp: true, |
|||
isUser: true, |
|||
} |
|||
: null |
|||
} |
|||
|
|||
export const buildUserPermissions = async (app, userAccessLevels) => { |
|||
const allAccessLevels = await _loadAccessLevels(app) |
|||
|
|||
return $(allAccessLevels.levels, [ |
|||
filter(l => some(ua => l.name === ua)(userAccessLevels)), |
|||
map(l => l.permissions), |
|||
flatten, |
|||
]) |
|||
} |
|||
@ -1,70 +0,0 @@ |
|||
import { generate } from "shortid" |
|||
import { |
|||
tempCodeExpiryLength, |
|||
USERS_LOCK_FILE, |
|||
USERS_LIST_FILE, |
|||
userAuthFile, |
|||
getUserByName, |
|||
} from "./authCommon" |
|||
import { getLock, isNolock, releaseLock } from "../common/lock" |
|||
import { apiWrapper, events } from "../common" |
|||
import { alwaysAuthorized } from "./permissions" |
|||
|
|||
export const createTemporaryAccess = app => async userName => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.createTemporaryAccess, |
|||
alwaysAuthorized, |
|||
{ userName }, |
|||
_createTemporaryAccess, |
|||
app, |
|||
userName |
|||
) |
|||
|
|||
export const _createTemporaryAccess = async (app, userName) => { |
|||
const tempCode = await getTemporaryCode(app) |
|||
|
|||
const lock = await getLock(app, USERS_LOCK_FILE, 1000, 2) |
|||
|
|||
if (isNolock(lock)) { |
|||
throw new Error( |
|||
"Unable to create temporary access, could not get lock - try again" |
|||
) |
|||
} |
|||
|
|||
try { |
|||
const users = await app.datastore.loadJson(USERS_LIST_FILE) |
|||
|
|||
const user = getUserByName(users, userName) |
|||
user.temporaryAccessId = tempCode.temporaryAccessId |
|||
|
|||
await app.datastore.updateJson(USERS_LIST_FILE, users) |
|||
} finally { |
|||
await releaseLock(app, lock) |
|||
} |
|||
|
|||
const userAuth = await app.datastore.loadJson(userAuthFile(userName)) |
|||
userAuth.temporaryAccessHash = tempCode.temporaryAccessHash |
|||
|
|||
userAuth.temporaryAccessExpiryEpoch = tempCode.temporaryAccessExpiryEpoch |
|||
|
|||
await app.datastore.updateJson(userAuthFile(userName), userAuth) |
|||
|
|||
return tempCode.tempCode |
|||
} |
|||
|
|||
export const getTemporaryCode = async app => { |
|||
const tempCode = generate() + generate() + generate() + generate() |
|||
|
|||
const tempId = generate() |
|||
|
|||
return { |
|||
temporaryAccessHash: app.crypto.hash(tempCode), |
|||
temporaryAccessExpiryEpoch: |
|||
(await app.getEpochTime()) + tempCodeExpiryLength, |
|||
tempCode: `tmp:${tempId}:${tempCode}`, |
|||
temporaryAccessId: tempId, |
|||
} |
|||
} |
|||
|
|||
export const looksLikeTemporaryCode = code => code.startsWith("tmp:") |
|||
@ -1,96 +0,0 @@ |
|||
import { join, some } from "lodash/fp" |
|||
import { validateUser } from "./validateUser" |
|||
import { getNewUserAuth } from "./getNewUser" |
|||
import { |
|||
getLock, |
|||
isNolock, |
|||
releaseLock, |
|||
apiWrapper, |
|||
events, |
|||
insensitiveEquals, |
|||
isNonEmptyString, |
|||
} from "../common" |
|||
import { |
|||
USERS_LOCK_FILE, |
|||
stripUserOfSensitiveStuff, |
|||
USERS_LIST_FILE, |
|||
userAuthFile, |
|||
} from "./authCommon" |
|||
import { getTemporaryCode } from "./createTemporaryAccess" |
|||
import { isValidPassword } from "./setPassword" |
|||
import { permission } from "./permissions" |
|||
import { BadRequestError } from "../common/errors" |
|||
|
|||
export const createUser = app => async (user, password = null) => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.createUser, |
|||
permission.createUser.isAuthorized, |
|||
{ user, password }, |
|||
_createUser, |
|||
app, |
|||
user, |
|||
password |
|||
) |
|||
|
|||
export const _createUser = async (app, user, password = null) => { |
|||
const lock = await getLock(app, USERS_LOCK_FILE, 1000, 2) |
|||
|
|||
if (isNolock(lock)) { |
|||
throw new Error("Unable to create user, could not get lock - try again") |
|||
} |
|||
|
|||
const users = await app.datastore.loadJson(USERS_LIST_FILE) |
|||
|
|||
const userErrors = validateUser(app)([...users, user], user) |
|||
if (userErrors.length > 0) { |
|||
throw new BadRequestError(`User is invalid. ${join("; ")(userErrors.map(e => e.error))}`) |
|||
} |
|||
|
|||
const { auth, tempCode, temporaryAccessId } = await getAccess(app, password) |
|||
user.tempCode = tempCode |
|||
user.temporaryAccessId = temporaryAccessId |
|||
|
|||
if (some(u => insensitiveEquals(u.name, user.name))(users)) { |
|||
throw new BadRequestError("User already exists") |
|||
} |
|||
|
|||
users.push(stripUserOfSensitiveStuff(user)) |
|||
|
|||
await app.datastore.updateJson(USERS_LIST_FILE, users) |
|||
|
|||
try { |
|||
await app.datastore.createJson(userAuthFile(user.name), auth) |
|||
} catch (_) { |
|||
await app.datastore.updateJson(userAuthFile(user.name), auth) |
|||
} |
|||
|
|||
await releaseLock(app, lock) |
|||
|
|||
return user |
|||
} |
|||
|
|||
const getAccess = async (app, password) => { |
|||
const auth = getNewUserAuth(app)() |
|||
|
|||
if (isNonEmptyString(password)) { |
|||
if (isValidPassword(password)) { |
|||
auth.passwordHash = app.crypto.hash(password) |
|||
auth.temporaryAccessHash = "" |
|||
auth.temporaryAccessId = "" |
|||
auth.temporaryAccessExpiryEpoch = 0 |
|||
return { auth } |
|||
} |
|||
throw new BadRequestError("Password does not meet requirements") |
|||
} else { |
|||
const tempAccess = await getTemporaryCode(app) |
|||
auth.temporaryAccessHash = tempAccess.temporaryAccessHash |
|||
auth.temporaryAccessExpiryEpoch = tempAccess.temporaryAccessExpiryEpoch |
|||
auth.passwordHash = "" |
|||
return { |
|||
auth, |
|||
tempCode: tempAccess.tempCode, |
|||
temporaryAccessId: tempAccess.temporaryAccessId, |
|||
} |
|||
} |
|||
} |
|||
@ -1,58 +0,0 @@ |
|||
import { getLock, isNolock, releaseLock } from "../common/lock" |
|||
import { USERS_LOCK_FILE, USERS_LIST_FILE, getUserByName } from "./authCommon" |
|||
import { apiWrapper, events } from "../common" |
|||
import { permission } from "./permissions" |
|||
import { NotFoundError } from "../common/errors" |
|||
|
|||
export const enableUser = app => async username => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.enableUser, |
|||
permission.enableDisableUser.isAuthorized, |
|||
{ username }, |
|||
_enableUser, |
|||
app, |
|||
username |
|||
) |
|||
|
|||
export const disableUser = app => async username => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.disableUser, |
|||
permission.enableDisableUser.isAuthorized, |
|||
{ username }, |
|||
_disableUser, |
|||
app, |
|||
username |
|||
) |
|||
|
|||
export const _enableUser = async (app, username) => |
|||
await toggleUser(app, username, true) |
|||
|
|||
export const _disableUser = async (app, username) => |
|||
await toggleUser(app, username, false) |
|||
|
|||
const toggleUser = async (app, username, enabled) => { |
|||
const lock = await getLock(app, USERS_LOCK_FILE, 1000, 1, 0) |
|||
|
|||
const actionName = enabled ? "enable" : "disable" |
|||
|
|||
if (isNolock(lock)) { |
|||
throw new Error(`Could not ${actionName} user - cannot get lock`) |
|||
} |
|||
|
|||
try { |
|||
const users = await app.datastore.loadJson(USERS_LIST_FILE) |
|||
const user = getUserByName(users, username) |
|||
if (!user) { |
|||
throw new NotFoundError(`Could not find user to ${actionName}`) |
|||
} |
|||
|
|||
if (user.enabled === !enabled) { |
|||
user.enabled = enabled |
|||
await app.datastore.updateJson(USERS_LIST_FILE, users) |
|||
} |
|||
} finally { |
|||
releaseLock(app, lock) |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
import { filter, values, each, keys } from "lodash/fp" |
|||
import { permission } from "./permissions" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
isIndex, |
|||
isModel, |
|||
} from "../templateApi/hierarchy" |
|||
import { $ } from "../common" |
|||
|
|||
export const generateFullPermissions = app => { |
|||
const allNodes = getFlattenedHierarchy(app.hierarchy) |
|||
const accessLevel = { permissions: [] } |
|||
|
|||
const recordNodes = $(allNodes, [filter(isModel)]) |
|||
|
|||
for (const n of recordNodes) { |
|||
permission.createRecord.add(n.nodeKey(), accessLevel) |
|||
permission.updateRecord.add(n.nodeKey(), accessLevel) |
|||
permission.deleteRecord.add(n.nodeKey(), accessLevel) |
|||
permission.readRecord.add(n.nodeKey(), accessLevel) |
|||
} |
|||
|
|||
const indexNodes = $(allNodes, [filter(isIndex)]) |
|||
|
|||
for (const n of indexNodes) { |
|||
permission.readIndex.add(n.nodeKey(), accessLevel) |
|||
} |
|||
|
|||
for (const a of keys(app.actions)) { |
|||
permission.executeAction.add(a, accessLevel) |
|||
} |
|||
|
|||
$(permission, [values, filter(p => !p.isNode), each(p => p.add(accessLevel))]) |
|||
|
|||
return accessLevel.permissions |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
export const getNewAccessLevel = () => () => ({ |
|||
name: "", |
|||
permissions: [], |
|||
default: false, |
|||
}) |
|||
@ -1,35 +0,0 @@ |
|||
import { apiWrapperSync, events } from "../common" |
|||
import { permission } from "./permissions" |
|||
|
|||
export const getNewUser = app => () => |
|||
apiWrapperSync( |
|||
app, |
|||
events.authApi.getNewUser, |
|||
permission.createUser.isAuthorized, |
|||
{}, |
|||
_getNewUser, |
|||
app |
|||
) |
|||
|
|||
export const _getNewUser = () => ({ |
|||
name: "", |
|||
accessLevels: [], |
|||
enabled: true, |
|||
temporaryAccessId: "", |
|||
}) |
|||
|
|||
export const getNewUserAuth = app => () => |
|||
apiWrapperSync( |
|||
app, |
|||
events.authApi.getNewUserAuth, |
|||
permission.createUser.isAuthorized, |
|||
{}, |
|||
_getNewUserAuth, |
|||
app |
|||
) |
|||
|
|||
export const _getNewUserAuth = () => ({ |
|||
passwordHash: "", |
|||
temporaryAccessHash: "", |
|||
temporaryAccessExpiryEpoch: 0, |
|||
}) |
|||
@ -1,19 +0,0 @@ |
|||
import { map } from "lodash/fp" |
|||
import { USERS_LIST_FILE, stripUserOfSensitiveStuff } from "./authCommon" |
|||
import { $, apiWrapper, events } from "../common" |
|||
import { permission } from "./permissions" |
|||
|
|||
export const getUsers = app => async () => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.getUsers, |
|||
permission.listUsers.isAuthorized, |
|||
{}, |
|||
_getUsers, |
|||
app |
|||
) |
|||
|
|||
export const _getUsers = async app => |
|||
$(await app.datastore.loadJson(USERS_LIST_FILE), [ |
|||
map(stripUserOfSensitiveStuff), |
|||
]) |
|||
@ -1,46 +0,0 @@ |
|||
import { authenticate, authenticateTemporaryAccess } from "./authenticate" |
|||
import { createTemporaryAccess } from "./createTemporaryAccess" |
|||
import { createUser } from "./createUser" |
|||
import { enableUser, disableUser } from "./enableUser" |
|||
import { loadAccessLevels } from "./loadAccessLevels" |
|||
import { getNewAccessLevel } from "./getNewAccessLevel" |
|||
import { getNewUser, getNewUserAuth } from "./getNewUser" |
|||
import { getUsers } from "./getUsers" |
|||
import { isAuthorized } from "./isAuthorized" |
|||
import { saveAccessLevels } from "./saveAccessLevels" |
|||
import { |
|||
changeMyPassword, |
|||
scorePassword, |
|||
setPasswordFromTemporaryCode, |
|||
isValidPassword, |
|||
} from "./setPassword" |
|||
import { validateUser } from "./validateUser" |
|||
import { validateAccessLevels } from "./validateAccessLevels" |
|||
import { generateFullPermissions } from "./generateFullPermissions" |
|||
import { setUserAccessLevels } from "./setUserAccessLevels" |
|||
|
|||
export const getAuthApi = app => ({ |
|||
authenticate: authenticate(app), |
|||
authenticateTemporaryAccess: authenticateTemporaryAccess(app), |
|||
createTemporaryAccess: createTemporaryAccess(app), |
|||
createUser: createUser(app), |
|||
loadAccessLevels: loadAccessLevels(app), |
|||
enableUser: enableUser(app), |
|||
disableUser: disableUser(app), |
|||
getNewAccessLevel: getNewAccessLevel(app), |
|||
getNewUser: getNewUser(app), |
|||
getNewUserAuth: getNewUserAuth(app), |
|||
getUsers: getUsers(app), |
|||
saveAccessLevels: saveAccessLevels(app), |
|||
isAuthorized: isAuthorized(app), |
|||
changeMyPassword: changeMyPassword(app), |
|||
setPasswordFromTemporaryCode: setPasswordFromTemporaryCode(app), |
|||
scorePassword, |
|||
isValidPassword: isValidPassword(app), |
|||
validateUser: validateUser(app), |
|||
validateAccessLevels: validateAccessLevels(app), |
|||
generateFullPermissions: () => generateFullPermissions(app), |
|||
setUserAccessLevels: setUserAccessLevels(app), |
|||
}) |
|||
|
|||
export default getAuthApi |
|||
@ -1,44 +0,0 @@ |
|||
import { values, includes, some } from "lodash/fp" |
|||
import { permissionTypes } from "./authCommon" |
|||
import { $, isNothing, apiWrapperSync, events } from "../common" |
|||
import { getNodeByKeyOrNodeKey, isNode } from "../templateApi/hierarchy" |
|||
import { alwaysAuthorized } from "./permissions" |
|||
|
|||
export const isAuthorized = app => (permissionType, resourceKey) => |
|||
apiWrapperSync( |
|||
app, |
|||
events.authApi.isAuthorized, |
|||
alwaysAuthorized, |
|||
{ resourceKey, permissionType }, |
|||
_isAuthorized, |
|||
app, |
|||
permissionType, |
|||
resourceKey |
|||
) |
|||
|
|||
export const _isAuthorized = (app, permissionType, resourceKey) => { |
|||
if (!app.user) { |
|||
return false |
|||
} |
|||
|
|||
const validType = $(permissionTypes, [values, includes(permissionType)]) |
|||
|
|||
if (!validType) { |
|||
return false |
|||
} |
|||
|
|||
const permMatchesResource = userperm => { |
|||
const nodeKey = isNothing(resourceKey) |
|||
? null |
|||
: isNode(app.hierarchy, resourceKey) |
|||
? getNodeByKeyOrNodeKey(app.hierarchy, resourceKey).nodeKey() |
|||
: resourceKey |
|||
|
|||
return ( |
|||
userperm.type === permissionType && |
|||
(isNothing(resourceKey) || nodeKey === userperm.nodeKey) |
|||
) |
|||
} |
|||
|
|||
return $(app.user.permissions, [some(permMatchesResource)]) |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
import { ACCESS_LEVELS_FILE } from "./authCommon" |
|||
import { apiWrapper, events } from "../common" |
|||
import { permission } from "./permissions" |
|||
|
|||
export const loadAccessLevels = app => async () => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.loadAccessLevels, |
|||
permission.listAccessLevels.isAuthorized, |
|||
{}, |
|||
_loadAccessLevels, |
|||
app |
|||
) |
|||
|
|||
export const _loadAccessLevels = async app => |
|||
await app.datastore.loadJson(ACCESS_LEVELS_FILE) |
|||
@ -1,81 +0,0 @@ |
|||
import { permissionTypes } from "./authCommon" |
|||
import { isAuthorized } from "./isAuthorized" |
|||
|
|||
export const temporaryAccessPermissions = () => [ |
|||
{ type: permissionTypes.SET_PASSWORD }, |
|||
] |
|||
|
|||
const nodePermission = type => ({ |
|||
add: (nodeKey, accessLevel) => |
|||
accessLevel.permissions.push({ type, nodeKey }), |
|||
isAuthorized: resourceKey => app => isAuthorized(app)(type, resourceKey), |
|||
isNode: true, |
|||
get: nodeKey => ({ type, nodeKey }), |
|||
}) |
|||
|
|||
const staticPermission = type => ({ |
|||
add: accessLevel => accessLevel.permissions.push({ type }), |
|||
isAuthorized: app => isAuthorized(app)(type), |
|||
isNode: false, |
|||
get: () => ({ type }), |
|||
}) |
|||
|
|||
const createRecord = nodePermission(permissionTypes.CREATE_RECORD) |
|||
|
|||
const updateRecord = nodePermission(permissionTypes.UPDATE_RECORD) |
|||
|
|||
const deleteRecord = nodePermission(permissionTypes.DELETE_RECORD) |
|||
|
|||
const readRecord = nodePermission(permissionTypes.READ_RECORD) |
|||
|
|||
const writeTemplates = staticPermission(permissionTypes.WRITE_TEMPLATES) |
|||
|
|||
const createUser = staticPermission(permissionTypes.CREATE_USER) |
|||
|
|||
const setPassword = staticPermission(permissionTypes.SET_PASSWORD) |
|||
|
|||
const readIndex = nodePermission(permissionTypes.READ_INDEX) |
|||
|
|||
const manageIndex = staticPermission(permissionTypes.MANAGE_INDEX) |
|||
|
|||
const manageCollection = staticPermission(permissionTypes.MANAGE_COLLECTION) |
|||
|
|||
const createTemporaryAccess = staticPermission( |
|||
permissionTypes.CREATE_TEMPORARY_ACCESS |
|||
) |
|||
|
|||
const enableDisableUser = staticPermission(permissionTypes.ENABLE_DISABLE_USER) |
|||
|
|||
const writeAccessLevels = staticPermission(permissionTypes.WRITE_ACCESS_LEVELS) |
|||
|
|||
const listUsers = staticPermission(permissionTypes.LIST_USERS) |
|||
|
|||
const listAccessLevels = staticPermission(permissionTypes.LIST_ACCESS_LEVELS) |
|||
|
|||
const setUserAccessLevels = staticPermission( |
|||
permissionTypes.SET_USER_ACCESS_LEVELS |
|||
) |
|||
|
|||
const executeAction = nodePermission(permissionTypes.EXECUTE_ACTION) |
|||
|
|||
export const alwaysAuthorized = () => true |
|||
|
|||
export const permission = { |
|||
createRecord, |
|||
updateRecord, |
|||
deleteRecord, |
|||
readRecord, |
|||
writeTemplates, |
|||
createUser, |
|||
setPassword, |
|||
readIndex, |
|||
createTemporaryAccess, |
|||
enableDisableUser, |
|||
writeAccessLevels, |
|||
listUsers, |
|||
listAccessLevels, |
|||
manageIndex, |
|||
manageCollection, |
|||
executeAction, |
|||
setUserAccessLevels, |
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
import { join, map } from "lodash/fp" |
|||
import { |
|||
getLock, |
|||
releaseLock, |
|||
$, |
|||
isNolock, |
|||
apiWrapper, |
|||
events, |
|||
} from "../common" |
|||
import { ACCESS_LEVELS_LOCK_FILE, ACCESS_LEVELS_FILE } from "./authCommon" |
|||
import { validateAccessLevels } from "./validateAccessLevels" |
|||
import { permission } from "./permissions" |
|||
|
|||
export const saveAccessLevels = app => async accessLevels => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.saveAccessLevels, |
|||
permission.writeAccessLevels.isAuthorized, |
|||
{ accessLevels }, |
|||
_saveAccessLevels, |
|||
app, |
|||
accessLevels |
|||
) |
|||
|
|||
export const _saveAccessLevels = async (app, accessLevels) => { |
|||
const validationErrors = validateAccessLevels(app)(accessLevels.levels) |
|||
if (validationErrors.length > 0) { |
|||
const errs = $(validationErrors, [map(e => e.error), join(", ")]) |
|||
throw new Error(`Access Levels Invalid: ${errs}`) |
|||
} |
|||
|
|||
const lock = await getLock(app, ACCESS_LEVELS_LOCK_FILE, 2000, 2) |
|||
|
|||
if (isNolock(lock)) { |
|||
throw new Error("Could not get lock to save access levels") |
|||
} |
|||
|
|||
try { |
|||
const existing = await app.datastore.loadJson(ACCESS_LEVELS_FILE) |
|||
if (existing.version !== accessLevels.version) { |
|||
throw new Error( |
|||
"Access levels have already been updated, since you loaded" |
|||
) |
|||
} |
|||
|
|||
accessLevels.version++ |
|||
|
|||
app.datastore.updateJson(ACCESS_LEVELS_FILE, accessLevels) |
|||
} finally { |
|||
await releaseLock(app, lock) |
|||
} |
|||
} |
|||
@ -1,160 +0,0 @@ |
|||
import { find } from "lodash/fp" |
|||
import { userAuthFile, parseTemporaryCode } from "./authCommon" |
|||
import { isSomething, $, apiWrapper, apiWrapperSync, events } from "../common" |
|||
import { _getUsers } from "./getUsers" |
|||
import { alwaysAuthorized } from "./permissions" |
|||
|
|||
export const isValidPassword = app => password => |
|||
apiWrapperSync( |
|||
app, |
|||
events.authApi.isValidPassword, |
|||
alwaysAuthorized, |
|||
{ password }, |
|||
_isValidPassword, |
|||
app, |
|||
password |
|||
) |
|||
|
|||
export const _isValidPassword = (app, password) => |
|||
scorePassword(password).score > 30 |
|||
|
|||
export const changeMyPassword = app => async (currentPw, newpassword) => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.changeMyPassword, |
|||
alwaysAuthorized, |
|||
{ currentPw, newpassword }, |
|||
_changeMyPassword, |
|||
app, |
|||
currentPw, |
|||
newpassword |
|||
) |
|||
|
|||
export const _changeMyPassword = async (app, currentPw, newpassword) => { |
|||
const existingAuth = await app.datastore.loadJson(userAuthFile(app.user.name)) |
|||
|
|||
if (isSomething(existingAuth.passwordHash)) { |
|||
const verified = app.crypto.verify(existingAuth.passwordHash, currentPw) |
|||
|
|||
if (verified) { |
|||
await await doSet(app, existingAuth, app.user.name, newpassword) |
|||
return true |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
export const setPasswordFromTemporaryCode = app => async ( |
|||
tempCode, |
|||
newpassword |
|||
) => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.setPasswordFromTemporaryCode, |
|||
alwaysAuthorized, |
|||
{ tempCode, newpassword }, |
|||
_setPasswordFromTemporaryCode, |
|||
app, |
|||
tempCode, |
|||
newpassword |
|||
) |
|||
|
|||
export const _setPasswordFromTemporaryCode = async ( |
|||
app, |
|||
tempCode, |
|||
newpassword |
|||
) => { |
|||
const currentTime = await app.getEpochTime() |
|||
|
|||
const temp = parseTemporaryCode(tempCode) |
|||
|
|||
const user = $(await _getUsers(app), [ |
|||
find(u => u.temporaryAccessId === temp.id), |
|||
]) |
|||
|
|||
if (!user) { |
|||
return false |
|||
} |
|||
|
|||
const existingAuth = await app.datastore.loadJson(userAuthFile(user.name)) |
|||
|
|||
if ( |
|||
isSomething(existingAuth.temporaryAccessHash) && |
|||
existingAuth.temporaryAccessExpiryEpoch > currentTime |
|||
) { |
|||
const verified = app.crypto.verify( |
|||
existingAuth.temporaryAccessHash, |
|||
temp.code |
|||
) |
|||
|
|||
if (verified) { |
|||
await doSet(app, existingAuth, user.name, newpassword) |
|||
return true |
|||
} |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
const doSet = async (app, auth, username, newpassword) => { |
|||
auth.temporaryAccessHash = "" |
|||
auth.temporaryAccessExpiryEpoch = 0 |
|||
auth.passwordHash = app.crypto.hash(newpassword) |
|||
await app.datastore.updateJson(userAuthFile(username), auth) |
|||
} |
|||
|
|||
export const scorePassword = app => password => |
|||
apiWrapperSync( |
|||
app, |
|||
events.authApi.scorePassword, |
|||
alwaysAuthorized, |
|||
{ password }, |
|||
_scorePassword, |
|||
password |
|||
) |
|||
|
|||
export const _scorePassword = password => { |
|||
// from https://stackoverflow.com/questions/948172/password-strength-meter
|
|||
// thank you https://stackoverflow.com/users/46617/tm-lv
|
|||
|
|||
let score = 0 |
|||
if (!password) { |
|||
return score |
|||
} |
|||
|
|||
// award every unique letter until 5 repetitions
|
|||
const letters = new Object() |
|||
for (let i = 0; i < password.length; i++) { |
|||
letters[password[i]] = (letters[password[i]] || 0) + 1 |
|||
score += 5.0 / letters[password[i]] |
|||
} |
|||
|
|||
// bonus points for mixing it up
|
|||
const variations = { |
|||
digits: /\d/.test(password), |
|||
lower: /[a-z]/.test(password), |
|||
upper: /[A-Z]/.test(password), |
|||
nonWords: /\W/.test(password), |
|||
} |
|||
|
|||
let variationCount = 0 |
|||
for (const check in variations) { |
|||
variationCount += variations[check] == true ? 1 : 0 |
|||
} |
|||
score += (variationCount - 1) * 10 |
|||
|
|||
const strengthText = |
|||
score > 80 |
|||
? "strong" |
|||
: score > 60 |
|||
? "good" |
|||
: score >= 30 |
|||
? "weak" |
|||
: "very weak" |
|||
|
|||
return { |
|||
score: parseInt(score), |
|||
strengthText, |
|||
} |
|||
} |
|||
@ -1,60 +0,0 @@ |
|||
import { difference, map, join } from "lodash/fp" |
|||
import { |
|||
getLock, |
|||
isNolock, |
|||
releaseLock, |
|||
$, |
|||
apiWrapper, |
|||
events, |
|||
} from "../common" |
|||
import { |
|||
USERS_LOCK_FILE, |
|||
ACCESS_LEVELS_FILE, |
|||
getUserByName, |
|||
USERS_LIST_FILE, |
|||
} from "./authCommon" |
|||
import { permission } from "./permissions" |
|||
import { NotFoundError } from "../common/errors" |
|||
|
|||
export const setUserAccessLevels = app => async (userName, accessLevels) => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.setUserAccessLevels, |
|||
permission.setUserAccessLevels.isAuthorized, |
|||
{ userName, accessLevels }, |
|||
_setUserAccessLevels, |
|||
app, |
|||
userName, |
|||
accessLevels |
|||
) |
|||
|
|||
export const _setUserAccessLevels = async (app, username, accessLevels) => { |
|||
const lock = await getLock(app, USERS_LOCK_FILE, 1000, 1, 0) |
|||
|
|||
const actualAccessLevels = $( |
|||
await app.datastore.loadJson(ACCESS_LEVELS_FILE), |
|||
[l => l.levels, map(l => l.name)] |
|||
) |
|||
|
|||
const missing = difference(accessLevels)(actualAccessLevels) |
|||
if (missing.length > 0) { |
|||
throw new Error(`Invalid access levels supplied: ${join(", ", missing)}`) |
|||
} |
|||
|
|||
if (isNolock(lock)) { |
|||
throw new Error("Could set user access levels cannot get lock") |
|||
} |
|||
|
|||
try { |
|||
const users = await app.datastore.loadJson(USERS_LIST_FILE) |
|||
const user = getUserByName(users, username) |
|||
if (!user) { |
|||
throw new NotFoundError(`Could not find user with ${username}`) |
|||
} |
|||
|
|||
user.accessLevels = accessLevels |
|||
await app.datastore.updateJson(USERS_LIST_FILE, users) |
|||
} finally { |
|||
releaseLock(app, lock) |
|||
} |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
import { |
|||
values, |
|||
includes, |
|||
map, |
|||
concat, |
|||
isEmpty, |
|||
uniqWith, |
|||
some, |
|||
flatten, |
|||
filter, |
|||
} from "lodash/fp" |
|||
import { applyRuleSet, makerule } from "../common/validationCommon" |
|||
import { permissionTypes } from "./authCommon" |
|||
import { |
|||
$, |
|||
isSomething, |
|||
insensitiveEquals, |
|||
isNonEmptyString, |
|||
apiWrapperSync, |
|||
events, |
|||
} from "../common" |
|||
import { getNode } from "../templateApi/hierarchy" |
|||
import { alwaysAuthorized } from "./permissions" |
|||
|
|||
const isAllowedType = t => $(permissionTypes, [values, includes(t)]) |
|||
|
|||
const isModelOrIndexType = t => |
|||
some(p => p === t)([ |
|||
permissionTypes.CREATE_RECORD, |
|||
permissionTypes.UPDATE_RECORD, |
|||
permissionTypes.DELETE_RECORD, |
|||
permissionTypes.READ_RECORD, |
|||
permissionTypes.READ_INDEX, |
|||
permissionTypes.EXECUTE_ACTION, |
|||
]) |
|||
|
|||
const permissionRules = app => [ |
|||
makerule("type", "type must be one of allowed types", p => |
|||
isAllowedType(p.type) |
|||
), |
|||
makerule( |
|||
"nodeKey", |
|||
"record and index permissions must include a valid nodeKey", |
|||
p => |
|||
!isModelOrIndexType(p.type) || |
|||
isSomething(getNode(app.hierarchy, p.nodeKey)) |
|||
), |
|||
] |
|||
|
|||
const applyPermissionRules = app => applyRuleSet(permissionRules(app)) |
|||
|
|||
const accessLevelRules = allLevels => [ |
|||
makerule("name", "name must be set", l => isNonEmptyString(l.name)), |
|||
makerule( |
|||
"name", |
|||
"access level names must be unique", |
|||
l => |
|||
isEmpty(l.name) || |
|||
filter(a => insensitiveEquals(l.name, a.name))(allLevels).length === 1 |
|||
), |
|||
] |
|||
|
|||
const applyLevelRules = allLevels => applyRuleSet(accessLevelRules(allLevels)) |
|||
|
|||
export const validateAccessLevel = app => (allLevels, level) => { |
|||
const errs = $(level.permissions, [ |
|||
map(applyPermissionRules(app)), |
|||
flatten, |
|||
concat(applyLevelRules(allLevels)(level)), |
|||
]) |
|||
|
|||
return errs |
|||
} |
|||
|
|||
export const validateAccessLevels = app => allLevels => |
|||
apiWrapperSync( |
|||
app, |
|||
events.authApi.validateAccessLevels, |
|||
alwaysAuthorized, |
|||
{ allLevels }, |
|||
_validateAccessLevels, |
|||
app, |
|||
allLevels |
|||
) |
|||
|
|||
export const _validateAccessLevels = (app, allLevels) => |
|||
$(allLevels, [ |
|||
map(l => validateAccessLevel(app)(allLevels, l)), |
|||
flatten, |
|||
uniqWith( |
|||
(x, y) => x.field === y.field && x.item === y.item && x.error === y.error |
|||
), |
|||
]) |
|||
@ -1,51 +0,0 @@ |
|||
import { map, uniqWith, flatten, filter } from "lodash/fp" |
|||
import { applyRuleSet, makerule } from "../common/validationCommon" |
|||
import { |
|||
$, |
|||
insensitiveEquals, |
|||
apiWrapper, |
|||
events, |
|||
isNonEmptyString, |
|||
all, |
|||
} from "../common" |
|||
import { alwaysAuthorized } from "./permissions" |
|||
|
|||
const userRules = allUsers => [ |
|||
makerule("name", "username must be set", u => isNonEmptyString(u.name)), |
|||
makerule( |
|||
"accessLevels", |
|||
"user must have at least one access level", |
|||
u => u.accessLevels.length > 0 |
|||
), |
|||
makerule( |
|||
"name", |
|||
"username must be unique", |
|||
u => filter(u2 => insensitiveEquals(u2.name, u.name))(allUsers).length === 1 |
|||
), |
|||
makerule("accessLevels", "access levels must only contain stings", u => |
|||
all(isNonEmptyString)(u.accessLevels) |
|||
), |
|||
] |
|||
|
|||
export const validateUser = () => (allusers, user) => |
|||
applyRuleSet(userRules(allusers))(user) |
|||
|
|||
export const validateUsers = app => allUsers => |
|||
apiWrapper( |
|||
app, |
|||
events.authApi.validateUsers, |
|||
alwaysAuthorized, |
|||
{ allUsers }, |
|||
_validateUsers, |
|||
app, |
|||
allUsers |
|||
) |
|||
|
|||
export const _validateUsers = (app, allUsers) => |
|||
$(allUsers, [ |
|||
map(l => validateUser(app)(allUsers, l)), |
|||
flatten, |
|||
uniqWith( |
|||
(x, y) => x.field === y.field && x.item === y.item && x.error === y.error |
|||
), |
|||
]) |
|||
@ -1,57 +0,0 @@ |
|||
import { safeKey, apiWrapper, events, joinKey } from "../common" |
|||
import { _deleteRecord } from "../recordApi/delete" |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
import { permission } from "../authApi/permissions" |
|||
import { getCollectionDir } from "../recordApi/recordInfo" |
|||
import { ensureCollectionIsInitialised } from "./initialise" |
|||
import { getNodeForCollectionPath } from "../templateApi/hierarchy" |
|||
|
|||
export const deleteCollection = (app, disableCleanup = false) => async key => |
|||
apiWrapper( |
|||
app, |
|||
events.collectionApi.delete, |
|||
permission.manageCollection.isAuthorized, |
|||
{ key }, |
|||
_deleteCollection, |
|||
app, |
|||
key, |
|||
disableCleanup |
|||
) |
|||
|
|||
/* |
|||
const recordNode = getCollectionNode(app.hierarchy, key); |
|||
|
|||
*/ |
|||
|
|||
export const _deleteCollection = async (app, key, disableCleanup) => { |
|||
key = safeKey(key) |
|||
const collectionDir = getCollectionDir(app.hierarchy, key) |
|||
await deleteRecords(app, key) |
|||
await deleteCollectionFolder(app, key, collectionDir) |
|||
if (!disableCleanup) { |
|||
await app.cleanupTransactions() |
|||
} |
|||
} |
|||
|
|||
const deleteCollectionFolder = async (app, key, dir) => { |
|||
await app.datastore.deleteFolder(dir) |
|||
await ensureCollectionIsInitialised( |
|||
app.datastore, |
|||
getNodeForCollectionPath(app.hierarchy)(key), |
|||
dir) |
|||
} |
|||
|
|||
const deleteRecords = async (app, key) => { |
|||
const iterate = await getAllIdsIterator(app)(key) |
|||
|
|||
let ids = await iterate() |
|||
while (!ids.done) { |
|||
if (ids.result.collectionKey === key) { |
|||
for (const id of ids.result.ids) { |
|||
await _deleteRecord(app, joinKey(key, id), true) |
|||
} |
|||
} |
|||
|
|||
ids = await iterate() |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
import { getNodeForCollectionPath } from "../templateApi/hierarchy" |
|||
import { isNothing, safeKey, apiWrapperSync, events } from "../common" |
|||
import { alwaysAuthorized } from "../authApi/permissions" |
|||
|
|||
export const getAllowedRecordTypes = app => key => |
|||
apiWrapperSync( |
|||
app, |
|||
events.collectionApi.getAllowedRecordTypes, |
|||
alwaysAuthorized, |
|||
{ key }, |
|||
_getAllowedRecordTypes, |
|||
app, |
|||
key |
|||
) |
|||
|
|||
const _getAllowedRecordTypes = (app, key) => { |
|||
key = safeKey(key) |
|||
const node = getNodeForCollectionPath(app.hierarchy)(key) |
|||
return isNothing(node) ? [] : [node.name] |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
import { getAllowedRecordTypes } from "./getAllowedRecordTypes" |
|||
import { deleteCollection } from "./delete" |
|||
|
|||
export const getCollectionApi = app => ({ |
|||
getAllowedRecordTypes: getAllowedRecordTypes(app), |
|||
getAllIdsIterator: getAllIdsIterator(app), |
|||
delete: deleteCollection(app), |
|||
}) |
|||
|
|||
export default getCollectionApi |
|||
@ -1,48 +0,0 @@ |
|||
import { filter } from "lodash/fp" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
isCollectionRecord, |
|||
isRoot, |
|||
} from "../templateApi/hierarchy" |
|||
import { $, allTrue, joinKey } from "../common" |
|||
|
|||
export const ensureCollectionIsInitialised = async (datastore, node, dir) => { |
|||
if (!(await datastore.exists(dir))) { |
|||
await datastore.createFolder(dir) |
|||
await datastore.createFolder(joinKey(dir, node.nodeId)) |
|||
} |
|||
} |
|||
|
|||
export const initialiseRootCollections = async (datastore, hierarchy) => { |
|||
const rootCollectionRecord = allTrue( |
|||
n => isRoot(n.parent()), |
|||
isCollectionRecord |
|||
) |
|||
|
|||
const flathierarchy = getFlattenedHierarchy(hierarchy) |
|||
|
|||
const collectionRecords = $(flathierarchy, [filter(rootCollectionRecord)]) |
|||
|
|||
for (const col of collectionRecords) { |
|||
await ensureCollectionIsInitialised( |
|||
datastore, |
|||
col, |
|||
col.collectionPathRegx() |
|||
) |
|||
} |
|||
} |
|||
|
|||
export const initialiseChildCollections = async (app, recordInfo) => { |
|||
const childCollectionRecords = $(recordInfo.recordNode, [ |
|||
n => n.children, |
|||
filter(isCollectionRecord), |
|||
]) |
|||
|
|||
for (const child of childCollectionRecords) { |
|||
await ensureCollectionIsInitialised( |
|||
app.datastore, |
|||
child, |
|||
recordInfo.child(child.collectionName) |
|||
) |
|||
} |
|||
} |
|||
@ -1,128 +0,0 @@ |
|||
import { cloneDeep, isUndefined } from "lodash/fp" |
|||
import { generate } from "shortid" |
|||
import { UnauthorisedError } from "./errors" |
|||
|
|||
export const apiWrapper = async ( |
|||
app, |
|||
eventNamespace, |
|||
isAuthorized, |
|||
eventContext, |
|||
func, |
|||
...params |
|||
) => { |
|||
pushCallStack(app, eventNamespace) |
|||
|
|||
if (!isAuthorized(app)) { |
|||
handleNotAuthorized(app, eventContext, eventNamespace) |
|||
return |
|||
} |
|||
|
|||
const startDate = Date.now() |
|||
const elapsed = () => Date.now() - startDate |
|||
|
|||
try { |
|||
await app.publish(eventNamespace.onBegin, eventContext) |
|||
|
|||
const result = await func(...params) |
|||
|
|||
await publishComplete(app, eventContext, eventNamespace, elapsed, result) |
|||
return result |
|||
} catch (error) { |
|||
await publishError(app, eventContext, eventNamespace, elapsed, error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
export const apiWrapperSync = ( |
|||
app, |
|||
eventNamespace, |
|||
isAuthorized, |
|||
eventContext, |
|||
func, |
|||
...params |
|||
) => { |
|||
pushCallStack(app, eventNamespace) |
|||
|
|||
if (!isAuthorized(app)) { |
|||
handleNotAuthorized(app, eventContext, eventNamespace) |
|||
return |
|||
} |
|||
|
|||
const startDate = Date.now() |
|||
const elapsed = () => Date.now() - startDate |
|||
|
|||
try { |
|||
app.publish(eventNamespace.onBegin, eventContext) |
|||
|
|||
const result = func(...params) |
|||
|
|||
publishComplete(app, eventContext, eventNamespace, elapsed, result) |
|||
return result |
|||
} catch (error) { |
|||
publishError(app, eventContext, eventNamespace, elapsed, error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
const handleNotAuthorized = (app, eventContext, eventNamespace) => { |
|||
const err = new UnauthorisedError(`Unauthorized: ${eventNamespace}`) |
|||
publishError(app, eventContext, eventNamespace, () => 0, err) |
|||
throw err |
|||
} |
|||
|
|||
const pushCallStack = (app, eventNamespace, seedCallId) => { |
|||
const callId = generate() |
|||
|
|||
const createCallStack = () => ({ |
|||
seedCallId: !isUndefined(seedCallId) ? seedCallId : callId, |
|||
threadCallId: callId, |
|||
stack: [], |
|||
}) |
|||
|
|||
if (isUndefined(app.calls)) { |
|||
app.calls = createCallStack() |
|||
} |
|||
|
|||
app.calls.stack.push({ |
|||
namespace: eventNamespace, |
|||
callId, |
|||
}) |
|||
} |
|||
|
|||
const popCallStack = app => { |
|||
app.calls.stack.pop() |
|||
if (app.calls.stack.length === 0) { |
|||
delete app.calls |
|||
} |
|||
} |
|||
|
|||
const publishError = async ( |
|||
app, |
|||
eventContext, |
|||
eventNamespace, |
|||
elapsed, |
|||
err |
|||
) => { |
|||
const ctx = cloneDeep(eventContext) |
|||
ctx.error = err |
|||
ctx.elapsed = elapsed() |
|||
await app.publish(eventNamespace.onError, ctx) |
|||
popCallStack(app) |
|||
} |
|||
|
|||
const publishComplete = async ( |
|||
app, |
|||
eventContext, |
|||
eventNamespace, |
|||
elapsed, |
|||
result |
|||
) => { |
|||
const endcontext = cloneDeep(eventContext) |
|||
endcontext.result = result |
|||
endcontext.elapsed = elapsed() |
|||
await app.publish(eventNamespace.onComplete, endcontext) |
|||
popCallStack(app) |
|||
return result |
|||
} |
|||
|
|||
export default apiWrapper |
|||
@ -1,26 +0,0 @@ |
|||
import { compileCode as cCode } from "@nx-js/compiler-util" |
|||
import { includes } from "lodash/fp" |
|||
|
|||
export const compileCode = code => { |
|||
let func |
|||
let safeCode |
|||
|
|||
if (includes("return ")(code)) { |
|||
safeCode = code |
|||
} else { |
|||
let trimmed = code.trim() |
|||
trimmed = trimmed.endsWith(";") |
|||
? trimmed.substring(0, trimmed.length - 1) |
|||
: trimmed |
|||
safeCode = `return (${trimmed})` |
|||
} |
|||
|
|||
try { |
|||
func = cCode(safeCode) |
|||
} catch (e) { |
|||
e.message = `Error compiling code : ${code} : ${e.message}` |
|||
throw e |
|||
} |
|||
|
|||
return func |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
export class BadRequestError extends Error { |
|||
constructor(message) { |
|||
super(message) |
|||
this.httpStatusCode = 400 |
|||
} |
|||
} |
|||
|
|||
export class UnauthorisedError extends Error { |
|||
constructor(message) { |
|||
super(message) |
|||
this.httpStatusCode = 401 |
|||
} |
|||
} |
|||
|
|||
export class ForbiddenError extends Error { |
|||
constructor(message) { |
|||
super(message) |
|||
this.httpStatusCode = 403 |
|||
} |
|||
} |
|||
|
|||
export class NotFoundError extends Error { |
|||
constructor(message) { |
|||
super(message) |
|||
this.httpStatusCode = 404 |
|||
} |
|||
} |
|||
|
|||
export class ConflictError extends Error { |
|||
constructor(message) { |
|||
super(message) |
|||
this.httpStatusCode = 409 |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
import { union, reduce } from "lodash/fp" |
|||
|
|||
const commonPlus = extra => union(["onBegin", "onComplete", "onError"])(extra) |
|||
|
|||
const common = () => commonPlus([]) |
|||
|
|||
const _events = { |
|||
recordApi: { |
|||
save: commonPlus(["onInvalid", "onRecordUpdated", "onRecordCreated"]), |
|||
delete: common(), |
|||
getContext: common(), |
|||
getNew: common(), |
|||
load: common(), |
|||
validate: common(), |
|||
uploadFile: common(), |
|||
downloadFile: common(), |
|||
}, |
|||
authApi: { |
|||
authenticate: common(), |
|||
authenticateTemporaryAccess: common(), |
|||
createTemporaryAccess: common(), |
|||
createUser: common(), |
|||
enableUser: common(), |
|||
disableUser: common(), |
|||
loadAccessLevels: common(), |
|||
getNewAccessLevel: common(), |
|||
getNewUser: common(), |
|||
getNewUserAuth: common(), |
|||
getUsers: common(), |
|||
saveAccessLevels: common(), |
|||
isAuthorized: common(), |
|||
changeMyPassword: common(), |
|||
setPasswordFromTemporaryCode: common(), |
|||
scorePassword: common(), |
|||
isValidPassword: common(), |
|||
validateUser: common(), |
|||
validateAccessLevels: common(), |
|||
setUserAccessLevels: common(), |
|||
}, |
|||
templateApi: { |
|||
saveApplicationHierarchy: common(), |
|||
saveActionsAndTriggers: common(), |
|||
}, |
|||
actionsApi: { |
|||
execute: common(), |
|||
}, |
|||
} |
|||
|
|||
const _eventsList = [] |
|||
|
|||
const makeEvent = (area, method, name) => `${area}:${method}:${name}` |
|||
|
|||
for (const areaKey in _events) { |
|||
for (const methodKey in _events[areaKey]) { |
|||
_events[areaKey][methodKey] = reduce((obj, s) => { |
|||
obj[s] = makeEvent(areaKey, methodKey, s) |
|||
return obj |
|||
}, {})(_events[areaKey][methodKey]) |
|||
} |
|||
} |
|||
|
|||
for (const areaKey in _events) { |
|||
for (const methodKey in _events[areaKey]) { |
|||
for (const name in _events[areaKey][methodKey]) { |
|||
_eventsList.push(_events[areaKey][methodKey][name]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
export const events = _events |
|||
|
|||
export const eventsList = _eventsList |
|||
|
|||
export default { events: _events, eventsList: _eventsList } |
|||
@ -1,317 +0,0 @@ |
|||
import { |
|||
head, |
|||
tail, |
|||
findIndex, |
|||
startsWith, |
|||
dropRight, |
|||
flow, |
|||
takeRight, |
|||
trim, |
|||
replace, |
|||
} from "lodash" |
|||
import { |
|||
some, |
|||
reduce, |
|||
isEmpty, |
|||
isArray, |
|||
join, |
|||
isString, |
|||
isInteger, |
|||
isDate, |
|||
toNumber, |
|||
isUndefined, |
|||
isNaN, |
|||
isNull, |
|||
constant, |
|||
split, |
|||
includes, |
|||
filter, |
|||
} from "lodash/fp" |
|||
import { events, eventsList } from "./events" |
|||
import { apiWrapper } from "./apiWrapper" |
|||
import { getLock, NO_LOCK, isNolock } from "./lock" |
|||
import crypto from "./nodeCrypto" |
|||
|
|||
// this is the combinator function
|
|||
export const $$ = (...funcs) => arg => flow(funcs)(arg) |
|||
|
|||
// this is the pipe function
|
|||
export const $ = (arg, funcs) => $$(...funcs)(arg) |
|||
|
|||
export const keySep = "/" |
|||
const trimKeySep = str => trim(str, keySep) |
|||
const splitByKeySep = str => split(keySep)(str) |
|||
export const safeKey = key => |
|||
replace(`${keySep}${trimKeySep(key)}`, `${keySep}${keySep}`, keySep) |
|||
export const joinKey = (...strs) => { |
|||
const paramsOrArray = (strs.length === 1) & isArray(strs[0]) ? strs[0] : strs |
|||
return $(paramsOrArray, [ |
|||
filter(s => !isUndefined(s) && !isNull(s) && s.toString().length > 0), |
|||
join(keySep), |
|||
safeKey, |
|||
]) |
|||
} |
|||
export const splitKey = $$(trimKeySep, splitByKeySep) |
|||
export const getDirFomKey = $$(splitKey, dropRight, p => joinKey(...p)) |
|||
export const getFileFromKey = $$(splitKey, takeRight, head) |
|||
|
|||
export const configFolder = `${keySep}.config` |
|||
export const fieldDefinitions = joinKey(configFolder, "fields.json") |
|||
export const templateDefinitions = joinKey(configFolder, "templates.json") |
|||
export const appDefinitionFile = joinKey(configFolder, "appDefinition.json") |
|||
export const dirIndex = folderPath => |
|||
joinKey(configFolder, "dir", ...splitKey(folderPath), "dir.idx") |
|||
export const getIndexKeyFromFileKey = $$(getDirFomKey, dirIndex) |
|||
|
|||
export const ifExists = (val, exists, notExists) => |
|||
isUndefined(val) |
|||
? isUndefined(notExists) |
|||
? (() => {})() |
|||
: notExists() |
|||
: exists() |
|||
|
|||
export const getOrDefault = (val, defaultVal) => |
|||
ifExists( |
|||
val, |
|||
() => val, |
|||
() => defaultVal |
|||
) |
|||
|
|||
export const not = func => val => !func(val) |
|||
export const isDefined = not(isUndefined) |
|||
export const isNonNull = not(isNull) |
|||
export const isNotNaN = not(isNaN) |
|||
|
|||
export const allTrue = (...funcArgs) => val => |
|||
reduce( |
|||
(result, conditionFunc) => |
|||
(isNull(result) || result == true) && conditionFunc(val), |
|||
null |
|||
)(funcArgs) |
|||
|
|||
export const anyTrue = (...funcArgs) => val => |
|||
reduce( |
|||
(result, conditionFunc) => result == true || conditionFunc(val), |
|||
null |
|||
)(funcArgs) |
|||
|
|||
export const insensitiveEquals = (str1, str2) => |
|||
str1.trim().toLowerCase() === str2.trim().toLowerCase() |
|||
|
|||
export const isSomething = allTrue(isDefined, isNonNull, isNotNaN) |
|||
export const isNothing = not(isSomething) |
|||
export const isNothingOrEmpty = v => isNothing(v) || isEmpty(v) |
|||
export const somethingOrGetDefault = getDefaultFunc => val => |
|||
isSomething(val) ? val : getDefaultFunc() |
|||
export const somethingOrDefault = (val, defaultVal) => |
|||
somethingOrGetDefault(constant(defaultVal))(val) |
|||
|
|||
export const mapIfSomethingOrDefault = (mapFunc, defaultVal) => val => |
|||
isSomething(val) ? mapFunc(val) : defaultVal |
|||
|
|||
export const mapIfSomethingOrBlank = mapFunc => |
|||
mapIfSomethingOrDefault(mapFunc, "") |
|||
|
|||
export const none = predicate => collection => !some(predicate)(collection) |
|||
|
|||
export const all = predicate => collection => |
|||
none(v => !predicate(v))(collection) |
|||
|
|||
export const isNotEmpty = ob => !isEmpty(ob) |
|||
export const isAsync = fn => fn.constructor.name === "AsyncFunction" |
|||
export const isNonEmptyArray = allTrue(isArray, isNotEmpty) |
|||
export const isNonEmptyString = allTrue(isString, isNotEmpty) |
|||
export const tryOr = failFunc => (func, ...args) => { |
|||
try { |
|||
return func.apply(null, ...args) |
|||
} catch (_) { |
|||
return failFunc() |
|||
} |
|||
} |
|||
|
|||
export const tryAwaitOr = failFunc => async (func, ...args) => { |
|||
try { |
|||
return await func.apply(null, ...args) |
|||
} catch (_) { |
|||
return await failFunc() |
|||
} |
|||
} |
|||
|
|||
export const defineError = (func, errorPrefix) => { |
|||
try { |
|||
return func() |
|||
} catch (err) { |
|||
err.message = `${errorPrefix} : ${err.message}` |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
export const tryOrIgnore = tryOr(() => {}) |
|||
export const tryAwaitOrIgnore = tryAwaitOr(async () => {}) |
|||
export const causesException = func => { |
|||
try { |
|||
func() |
|||
return false |
|||
} catch (e) { |
|||
return true |
|||
} |
|||
} |
|||
|
|||
export const executesWithoutException = func => !causesException(func) |
|||
|
|||
export const handleErrorWith = returnValInError => |
|||
tryOr(constant(returnValInError)) |
|||
|
|||
export const handleErrorWithUndefined = handleErrorWith(undefined) |
|||
|
|||
export const switchCase = (...cases) => value => { |
|||
const nextCase = () => head(cases)[0](value) |
|||
const nextResult = () => head(cases)[1](value) |
|||
|
|||
if (isEmpty(cases)) return // undefined
|
|||
if (nextCase() === true) return nextResult() |
|||
return switchCase(...tail(cases))(value) |
|||
} |
|||
|
|||
export const isValue = val1 => val2 => val1 === val2 |
|||
export const isOneOf = (...vals) => val => includes(val)(vals) |
|||
export const defaultCase = constant(true) |
|||
export const memberMatches = (member, match) => obj => match(obj[member]) |
|||
|
|||
export const StartsWith = searchFor => searchIn => |
|||
startsWith(searchIn, searchFor) |
|||
|
|||
export const contains = val => array => findIndex(array, v => v === val) > -1 |
|||
|
|||
export const getHashCode = s => { |
|||
let hash = 0 |
|||
let i |
|||
let char |
|||
let l |
|||
if (s.length == 0) return hash |
|||
for (i = 0, l = s.length; i < l; i++) { |
|||
char = s.charCodeAt(i) |
|||
hash = (hash << 5) - hash + char |
|||
hash |= 0 // Convert to 32bit integer
|
|||
} |
|||
|
|||
// converting to string, but dont want a "-" prefixed
|
|||
if (hash < 0) { |
|||
return `n${(hash * -1).toString()}` |
|||
} |
|||
return hash.toString() |
|||
} |
|||
|
|||
// thanks to https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/
|
|||
export const awEx = async promise => { |
|||
try { |
|||
const result = await promise |
|||
return [undefined, result] |
|||
} catch (error) { |
|||
return [error, undefined] |
|||
} |
|||
} |
|||
|
|||
export const isSafeInteger = n => |
|||
isInteger(n) && |
|||
n <= Number.MAX_SAFE_INTEGER && |
|||
n >= 0 - Number.MAX_SAFE_INTEGER |
|||
|
|||
export const toDateOrNull = s => |
|||
isNull(s) ? null : isDate(s) ? s : new Date(s) |
|||
export const toBoolOrNull = s => (isNull(s) ? null : s === "true" || s === true) |
|||
export const toNumberOrNull = s => (isNull(s) ? null : toNumber(s)) |
|||
|
|||
export const isArrayOfString = opts => isArray(opts) && all(isString)(opts) |
|||
|
|||
export const pushAll = (target, items) => { |
|||
for (let i of items) target.push(i) |
|||
} |
|||
|
|||
export const pause = async duration => |
|||
new Promise(res => setTimeout(res, duration)) |
|||
|
|||
export const retry = async (fn, retries, delay, ...args) => { |
|||
try { |
|||
return await fn(...args) |
|||
} catch (err) { |
|||
if (retries > 1) { |
|||
return await pause(delay).then( |
|||
async () => await retry(fn, retries - 1, delay, ...args) |
|||
) |
|||
} |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
export { events } from "./events" |
|||
export { apiWrapper, apiWrapperSync } from "./apiWrapper" |
|||
export { getLock, NO_LOCK, releaseLock, extendLock, isNolock } from "./lock" |
|||
export { crypto } |
|||
|
|||
export default { |
|||
ifExists, |
|||
getOrDefault, |
|||
isDefined, |
|||
isNonNull, |
|||
isNotNaN, |
|||
allTrue, |
|||
isSomething, |
|||
mapIfSomethingOrDefault, |
|||
mapIfSomethingOrBlank, |
|||
configFolder, |
|||
fieldDefinitions, |
|||
isNothing, |
|||
not, |
|||
switchCase, |
|||
defaultCase, |
|||
StartsWith, |
|||
contains, |
|||
templateDefinitions, |
|||
handleErrorWith, |
|||
handleErrorWithUndefined, |
|||
tryOr, |
|||
tryOrIgnore, |
|||
tryAwaitOr, |
|||
tryAwaitOrIgnore, |
|||
dirIndex, |
|||
keySep, |
|||
$, |
|||
$$, |
|||
getDirFomKey, |
|||
getFileFromKey, |
|||
splitKey, |
|||
somethingOrDefault, |
|||
getIndexKeyFromFileKey, |
|||
joinKey, |
|||
somethingOrGetDefault, |
|||
appDefinitionFile, |
|||
isValue, |
|||
all, |
|||
isOneOf, |
|||
memberMatches, |
|||
defineError, |
|||
anyTrue, |
|||
isNonEmptyArray, |
|||
causesException, |
|||
executesWithoutException, |
|||
none, |
|||
getHashCode, |
|||
awEx, |
|||
apiWrapper, |
|||
events, |
|||
eventsList, |
|||
isNothingOrEmpty, |
|||
isSafeInteger, |
|||
toNumber, |
|||
toDate: toDateOrNull, |
|||
toBool: toBoolOrNull, |
|||
isArrayOfString, |
|||
getLock, |
|||
NO_LOCK, |
|||
isNolock, |
|||
insensitiveEquals, |
|||
pause, |
|||
retry, |
|||
pushAll, |
|||
} |
|||
@ -1,109 +0,0 @@ |
|||
import { split } from "lodash/fp" |
|||
import { $ } from "./index" |
|||
|
|||
const lockOverlapMilliseconds = 10 |
|||
|
|||
export const getLock = async ( |
|||
app, |
|||
lockFile, |
|||
timeoutMilliseconds, |
|||
maxLockRetries, |
|||
retryCount = 0 |
|||
) => { |
|||
try { |
|||
const timeout = (await app.getEpochTime()) + timeoutMilliseconds |
|||
|
|||
const lock = { |
|||
timeout, |
|||
key: lockFile, |
|||
totalTimeout: timeoutMilliseconds, |
|||
} |
|||
|
|||
await app.datastore.createFile( |
|||
lockFile, |
|||
getLockFileContent(lock.totalTimeout, lock.timeout) |
|||
) |
|||
|
|||
return lock |
|||
} catch (e) { |
|||
if (retryCount == maxLockRetries) { |
|||
return NO_LOCK |
|||
} |
|||
|
|||
const lock = parseLockFileContent( |
|||
lockFile, |
|||
await app.datastore.loadFile(lockFile) |
|||
) |
|||
|
|||
const currentEpochTime = await app.getEpochTime() |
|||
|
|||
if (currentEpochTime < lock.timeout) { |
|||
return NO_LOCK |
|||
} |
|||
|
|||
try { |
|||
await app.datastore.deleteFile(lockFile) |
|||
} catch (_) { |
|||
//empty
|
|||
} |
|||
|
|||
await sleepForRetry() |
|||
|
|||
return await getLock( |
|||
app, |
|||
lockFile, |
|||
timeoutMilliseconds, |
|||
maxLockRetries, |
|||
retryCount + 1 |
|||
) |
|||
} |
|||
} |
|||
|
|||
export const getLockFileContent = (totalTimeout, epochTime) => |
|||
`${totalTimeout}:${epochTime.toString()}` |
|||
|
|||
const parseLockFileContent = (key, content) => |
|||
$(content, [ |
|||
split(":"), |
|||
parts => ({ |
|||
totalTimeout: new Number(parts[0]), |
|||
timeout: new Number(parts[1]), |
|||
key, |
|||
}), |
|||
]) |
|||
|
|||
export const releaseLock = async (app, lock) => { |
|||
const currentEpochTime = await app.getEpochTime() |
|||
// only release if not timedout
|
|||
if (currentEpochTime < lock.timeout - lockOverlapMilliseconds) { |
|||
try { |
|||
await app.datastore.deleteFile(lock.key) |
|||
} catch (_) { |
|||
//empty
|
|||
} |
|||
} |
|||
} |
|||
|
|||
export const extendLock = async (app, lock) => { |
|||
const currentEpochTime = await app.getEpochTime() |
|||
// only release if not timedout
|
|||
if (currentEpochTime < lock.timeout - lockOverlapMilliseconds) { |
|||
try { |
|||
lock.timeout = currentEpochTime + lock.timeoutMilliseconds |
|||
await app.datastore.updateFile( |
|||
lock.key, |
|||
getLockFileContent(lock.totalTimeout, lock.timeout) |
|||
) |
|||
return lock |
|||
} catch (_) { |
|||
//empty
|
|||
} |
|||
} |
|||
return NO_LOCK |
|||
} |
|||
|
|||
export const NO_LOCK = "no lock" |
|||
export const isNolock = id => id === NO_LOCK |
|||
|
|||
const sleepForRetry = () => |
|||
new Promise(resolve => setTimeout(resolve, lockOverlapMilliseconds)) |
|||
@ -1,14 +0,0 @@ |
|||
import bcrypt from "bcryptjs" |
|||
|
|||
function hash(password) { |
|||
return bcrypt.hashSync(password, 10) |
|||
} |
|||
|
|||
function verify(hash, password) { |
|||
return bcrypt.compareSync(password, hash) |
|||
} |
|||
|
|||
export default { |
|||
hash, |
|||
verify, |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
import { filter, map } from "lodash/fp" |
|||
import { $, isSomething } from "./index" |
|||
|
|||
export const stringNotEmpty = s => isSomething(s) && s.trim().length > 0 |
|||
|
|||
export const makerule = (field, error, isValid) => ({ field, error, isValid }) |
|||
|
|||
export const validationError = (rule, item) => ({ ...rule, item }) |
|||
|
|||
export const applyRuleSet = ruleSet => itemToValidate => |
|||
$(ruleSet, [map(applyRule(itemToValidate)), filter(isSomething)]) |
|||
|
|||
export const applyRule = itemTovalidate => rule => |
|||
rule.isValid(itemTovalidate) ? null : validationError(rule, itemTovalidate) |
|||
@ -1,112 +0,0 @@ |
|||
import getRecordApi from "./recordApi" |
|||
import getCollectionApi from "./collectionApi" |
|||
import getIndexApi from "./indexApi" |
|||
import getTemplateApi from "./templateApi" |
|||
import getAuthApi from "./authApi" |
|||
import getActionsApi from "./actionsApi" |
|||
import { setupDatastore, createEventAggregator } from "./appInitialise" |
|||
import { initialiseActions } from "./actionsApi/initialise" |
|||
import { isSomething, crypto } from "./common" |
|||
import { setCleanupFunc } from "./transactions/setCleanupFunc" |
|||
import { generateFullPermissions } from "./authApi/generateFullPermissions" |
|||
import { getApplicationDefinition } from "./templateApi/getApplicationDefinition" |
|||
import common from "./common" |
|||
import { getBehaviourSources } from "./templateApi/getBehaviourSources" |
|||
import hierarchy from "./templateApi/hierarchy" |
|||
|
|||
export const getAppApis = async ( |
|||
store, |
|||
behaviourSources = null, |
|||
cleanupTransactions = null, |
|||
getEpochTime = null, |
|||
crypto = null, |
|||
appDefinition = null |
|||
) => { |
|||
store = setupDatastore(store) |
|||
|
|||
if (!appDefinition) appDefinition = await getApplicationDefinition(store)() |
|||
|
|||
if (!behaviourSources) behaviourSources = await getBehaviourSources(store) |
|||
|
|||
const eventAggregator = createEventAggregator() |
|||
|
|||
const app = { |
|||
datastore: store, |
|||
crypto, |
|||
publish: eventAggregator.publish, |
|||
hierarchy: appDefinition.hierarchy, |
|||
actions: appDefinition.actions, |
|||
} |
|||
|
|||
const templateApi = getTemplateApi(app) |
|||
|
|||
setCleanupFunc(app, cleanupTransactions) |
|||
|
|||
app.getEpochTime = isSomething(getEpochTime) |
|||
? getEpochTime |
|||
: async () => new Date().getTime() |
|||
|
|||
const recordApi = getRecordApi(app) |
|||
const collectionApi = getCollectionApi(app) |
|||
const indexApi = getIndexApi(app) |
|||
const authApi = getAuthApi(app) |
|||
const actionsApi = getActionsApi(app) |
|||
|
|||
const authenticateAs = async (username, password) => { |
|||
app.user = await authApi.authenticate(username, password) |
|||
} |
|||
|
|||
const withFullAccess = () => userWithFullAccess(app) |
|||
|
|||
const asUser = user => { |
|||
app.user = user |
|||
} |
|||
|
|||
let apis = { |
|||
recordApi, |
|||
templateApi, |
|||
collectionApi, |
|||
indexApi, |
|||
authApi, |
|||
actionsApi, |
|||
subscribe: eventAggregator.subscribe, |
|||
authenticateAs, |
|||
withFullAccess, |
|||
asUser, |
|||
} |
|||
|
|||
apis.actions = initialiseActions( |
|||
eventAggregator.subscribe, |
|||
behaviourSources, |
|||
appDefinition.actions, |
|||
appDefinition.triggers, |
|||
apis |
|||
) |
|||
|
|||
return apis |
|||
} |
|||
|
|||
export const userWithFullAccess = app => { |
|||
app.user = { |
|||
name: "app", |
|||
permissions: generateFullPermissions(app), |
|||
isUser: false, |
|||
temp: false, |
|||
} |
|||
return app.user |
|||
} |
|||
|
|||
export { events, eventsList } from "./common/events" |
|||
export { getTemplateApi } from "./templateApi" |
|||
export { getRecordApi } from "./recordApi" |
|||
export { getCollectionApi } from "./collectionApi" |
|||
export { getAuthApi } from "./authApi" |
|||
export { getIndexApi } from "./indexApi" |
|||
export { setupDatastore } from "./appInitialise" |
|||
export { getActionsApi } from "./actionsApi" |
|||
export { initialiseData } from "./appInitialise/initialiseData" |
|||
export { hierarchy } |
|||
export { common } |
|||
export { crypto } |
|||
|
|||
export default getAppApis |
|||
@ -1,180 +0,0 @@ |
|||
import { has, isNumber, isUndefined } from "lodash/fp" |
|||
import { compileCode } from "../common/compileCode" |
|||
import { safeKey, apiWrapper, events, isNonEmptyString } from "../common" |
|||
import { iterateIndex } from "../indexing/read" |
|||
import { |
|||
getUnshardedIndexDataKey, |
|||
getShardKeysInRange, |
|||
} from "../indexing/sharding" |
|||
import { |
|||
getExactNodeForKey, |
|||
isIndex, |
|||
isShardedIndex, |
|||
} from "../templateApi/hierarchy" |
|||
import { CONTINUE_READING_RECORDS } from "../indexing/serializer" |
|||
import { permission } from "../authApi/permissions" |
|||
import { BadRequestError } from "../common/errors" |
|||
import { getIndexDir } from "./getIndexDir" |
|||
|
|||
export const aggregates = app => async ( |
|||
indexKey, |
|||
rangeStartParams = null, |
|||
rangeEndParams = null |
|||
) => |
|||
apiWrapper( |
|||
app, |
|||
events.indexApi.aggregates, |
|||
permission.readIndex.isAuthorized(indexKey), |
|||
{ indexKey, rangeStartParams, rangeEndParams }, |
|||
_aggregates, |
|||
app, |
|||
indexKey, |
|||
rangeStartParams, |
|||
rangeEndParams |
|||
) |
|||
|
|||
const _aggregates = async (app, indexKey, rangeStartParams, rangeEndParams) => { |
|||
indexKey = safeKey(indexKey) |
|||
const indexNode = getExactNodeForKey(app.hierarchy)(indexKey) |
|||
const indexDir = getIndexDir(app.hierarchy, indexKey) |
|||
|
|||
if (!isIndex(indexNode)) { |
|||
throw new BadRequestError("supplied key is not an index") |
|||
} |
|||
|
|||
if (isShardedIndex(indexNode)) { |
|||
const shardKeys = await getShardKeysInRange( |
|||
app, |
|||
indexNode, |
|||
indexDir, |
|||
rangeStartParams, |
|||
rangeEndParams |
|||
) |
|||
let aggregateResult = null |
|||
for (const k of shardKeys) { |
|||
const shardResult = await getAggregates( |
|||
app.hierarchy, |
|||
app.datastore, |
|||
indexNode, |
|||
k |
|||
) |
|||
if (aggregateResult === null) { |
|||
aggregateResult = shardResult |
|||
} else { |
|||
aggregateResult = mergeShardAggregate(aggregateResult, shardResult) |
|||
} |
|||
} |
|||
return aggregateResult |
|||
} |
|||
return await getAggregates( |
|||
app.hierarchy, |
|||
app.datastore, |
|||
indexNode, |
|||
getUnshardedIndexDataKey(indexDir) |
|||
) |
|||
} |
|||
|
|||
const mergeShardAggregate = (totals, shard) => { |
|||
const mergeGrouping = (tot, shr) => { |
|||
tot.count += shr.count |
|||
for (const aggName in tot) { |
|||
if (aggName === "count") continue |
|||
const totagg = tot[aggName] |
|||
const shragg = shr[aggName] |
|||
totagg.sum += shragg.sum |
|||
totagg.max = totagg.max > shragg.max ? totagg.max : shragg.max |
|||
totagg.min = totagg.min < shragg.min ? totagg.min : shragg.min |
|||
totagg.mean = totagg.sum / tot.count |
|||
} |
|||
return tot |
|||
} |
|||
|
|||
for (const aggGroupDef in totals) { |
|||
for (const grouping in shard[aggGroupDef]) { |
|||
const groupingTotal = totals[aggGroupDef][grouping] |
|||
totals[aggGroupDef][grouping] = isUndefined(groupingTotal) |
|||
? shard[aggGroupDef][grouping] |
|||
: mergeGrouping( |
|||
totals[aggGroupDef][grouping], |
|||
shard[aggGroupDef][grouping] |
|||
) |
|||
} |
|||
} |
|||
|
|||
return totals |
|||
} |
|||
|
|||
const getAggregates = async (hierarchy, datastore, index, indexedDataKey) => { |
|||
const aggregateResult = {} |
|||
const doRead = iterateIndex( |
|||
async item => { |
|||
applyItemToAggregateResult(index, aggregateResult, item) |
|||
return CONTINUE_READING_RECORDS |
|||
}, |
|||
async () => aggregateResult |
|||
) |
|||
|
|||
return await doRead(hierarchy, datastore, index, indexedDataKey) |
|||
} |
|||
|
|||
const applyItemToAggregateResult = (indexNode, result, item) => { |
|||
const getInitialAggregateResult = () => ({ |
|||
sum: 0, |
|||
mean: null, |
|||
max: null, |
|||
min: null, |
|||
}) |
|||
|
|||
const applyAggregateResult = (agg, existing, count) => { |
|||
const value = compileCode(agg.aggregatedValue)({ record: item }) |
|||
|
|||
if (!isNumber(value)) return existing |
|||
|
|||
existing.sum += value |
|||
existing.max = |
|||
value > existing.max || existing.max === null ? value : existing.max |
|||
existing.min = |
|||
value < existing.min || existing.min === null ? value : existing.min |
|||
existing.mean = existing.sum / count |
|||
return existing |
|||
} |
|||
|
|||
for (const aggGroup of indexNode.aggregateGroups) { |
|||
if (!has(aggGroup.name)(result)) { |
|||
result[aggGroup.name] = {} |
|||
} |
|||
|
|||
const thisGroupResult = result[aggGroup.name] |
|||
|
|||
if (isNonEmptyString(aggGroup.condition)) { |
|||
if (!compileCode(aggGroup.condition)({ record: item })) { |
|||
continue |
|||
} |
|||
} |
|||
|
|||
let group = isNonEmptyString(aggGroup.groupBy) |
|||
? compileCode(aggGroup.groupBy)({ record: item }) |
|||
: "all" |
|||
if (!isNonEmptyString(group)) { |
|||
group = "(none)" |
|||
} |
|||
|
|||
if (!has(group)(thisGroupResult)) { |
|||
thisGroupResult[group] = { count: 0 } |
|||
for (const agg of aggGroup.aggregates) { |
|||
thisGroupResult[group][agg.name] = getInitialAggregateResult() |
|||
} |
|||
} |
|||
|
|||
thisGroupResult[group].count++ |
|||
|
|||
for (const agg of aggGroup.aggregates) { |
|||
const existingValues = thisGroupResult[group][agg.name] |
|||
thisGroupResult[group][agg.name] = applyAggregateResult( |
|||
agg, |
|||
existingValues, |
|||
thisGroupResult[group].count |
|||
) |
|||
} |
|||
} |
|||
} |
|||
@ -1,143 +0,0 @@ |
|||
import { filter, includes, some } from "lodash/fp" |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
getRecordNodeById, |
|||
getNode, |
|||
isIndex, |
|||
isModel, |
|||
getActualKeyOfParent, |
|||
getAllowedRecordNodesForIndex, |
|||
fieldReversesReferenceToIndex, |
|||
isTopLevelIndex, |
|||
} from "../templateApi/hierarchy" |
|||
import { joinKey, apiWrapper, events, $ } from "../common" |
|||
import { |
|||
createBuildIndexFolder, |
|||
transactionForBuildIndex, |
|||
} from "../transactions/create" |
|||
import { permission } from "../authApi/permissions" |
|||
import { BadRequestError } from "../common/errors" |
|||
import { initialiseIndex } from "../indexing/initialiseIndex" |
|||
import { getRecordInfo } from "../recordApi/recordInfo" |
|||
|
|||
/** rebuilds an index |
|||
* @param {object} app - the application container |
|||
* @param {string} indexNodeKey - node key of the index, which the index belongs to |
|||
*/ |
|||
export const buildIndex = app => async indexNodeKey => |
|||
apiWrapper( |
|||
app, |
|||
events.indexApi.buildIndex, |
|||
permission.manageIndex.isAuthorized, |
|||
{ indexNodeKey }, |
|||
_buildIndex, |
|||
app, |
|||
indexNodeKey |
|||
) |
|||
|
|||
export const _buildIndex = async (app, indexNodeKey) => { |
|||
const indexNode = getNode(app.hierarchy, indexNodeKey) |
|||
|
|||
await createBuildIndexFolder(app.datastore, indexNodeKey) |
|||
|
|||
if (!isIndex(indexNode)) { |
|||
throw new BadRequestError("BuildIndex: must supply an indexnode") |
|||
} |
|||
|
|||
if (indexNode.indexType === "reference") { |
|||
await buildReverseReferenceIndex(app, indexNode) |
|||
} else { |
|||
await buildHeirarchalIndex(app, indexNode) |
|||
} |
|||
|
|||
await app.cleanupTransactions() |
|||
} |
|||
|
|||
const buildReverseReferenceIndex = async (app, indexNode) => { |
|||
// Iterate through all referencING records,
|
|||
// and update referenced index for each record
|
|||
let recordCount = 0 |
|||
const referencingNodes = $(app.hierarchy, [ |
|||
getFlattenedHierarchy, |
|||
filter( |
|||
n => |
|||
isModel(n) && some(fieldReversesReferenceToIndex(indexNode))(n.fields) |
|||
), |
|||
]) |
|||
|
|||
const createTransactionsForReferencingNode = async referencingNode => { |
|||
const iterateReferencingNodes = await getAllIdsIterator(app)( |
|||
referencingNode.collectionNodeKey() |
|||
) |
|||
|
|||
let referencingIdIterator = await iterateReferencingNodes() |
|||
while (!referencingIdIterator.done) { |
|||
const { result } = referencingIdIterator |
|||
for (const id of result.ids) { |
|||
const recordKey = joinKey(result.collectionKey, id) |
|||
await transactionForBuildIndex( |
|||
app, |
|||
indexNode.nodeKey(), |
|||
recordKey, |
|||
recordCount |
|||
) |
|||
recordCount++ |
|||
} |
|||
referencingIdIterator = await iterateReferencingNodes() |
|||
} |
|||
} |
|||
|
|||
for (const referencingNode of referencingNodes) { |
|||
await createTransactionsForReferencingNode(referencingNode) |
|||
} |
|||
} |
|||
|
|||
const buildHeirarchalIndex = async (app, indexNode) => { |
|||
let recordCount = 0 |
|||
|
|||
const createTransactionsForIds = async (collectionKey, ids) => { |
|||
for (const recordId of ids) { |
|||
const recordKey = joinKey(collectionKey, recordId) |
|||
|
|||
const recordNode = getRecordNodeById(app.hierarchy, recordId) |
|||
|
|||
if (recordNodeApplies(indexNode)(recordNode)) { |
|||
await transactionForBuildIndex( |
|||
app, |
|||
indexNode.nodeKey(), |
|||
recordKey, |
|||
recordCount |
|||
) |
|||
recordCount++ |
|||
} |
|||
} |
|||
} |
|||
|
|||
const collectionRecords = getAllowedRecordNodesForIndex( |
|||
app.hierarchy, |
|||
indexNode |
|||
) |
|||
|
|||
for (const targetCollectionRecordNode of collectionRecords) { |
|||
const allIdsIterator = await getAllIdsIterator(app)( |
|||
targetCollectionRecordNode.collectionNodeKey() |
|||
) |
|||
|
|||
let allIds = await allIdsIterator() |
|||
while (allIds.done === false) { |
|||
await createTransactionsForIds( |
|||
allIds.result.collectionKey, |
|||
allIds.result.ids |
|||
) |
|||
allIds = await allIdsIterator() |
|||
} |
|||
} |
|||
|
|||
return recordCount |
|||
} |
|||
|
|||
const recordNodeApplies = indexNode => recordNode => |
|||
includes(recordNode.nodeId)(indexNode.allowedModelNodeIds) |
|||
|
|||
export default buildIndex |
|||
@ -1,38 +0,0 @@ |
|||
import { tryAwaitOrIgnore, safeKey } from "../common" |
|||
import { |
|||
isIndex, |
|||
isShardedIndex, |
|||
getExactNodeForKey, |
|||
} from "../templateApi/hierarchy" |
|||
import { |
|||
getAllShardKeys, |
|||
getShardMapKey, |
|||
getUnshardedIndexDataKey, |
|||
} from "../indexing/sharding" |
|||
import { getIndexDir } from "./getIndexDir" |
|||
|
|||
export const _deleteIndex = async (app, indexKey, includeFolder) => { |
|||
indexKey = safeKey(indexKey) |
|||
const indexNode = getExactNodeForKey(app.hierarchy)(indexKey) |
|||
const indexDir = getIndexDir(app.hierarchy, indexKey) |
|||
|
|||
if (!isIndex(indexNode)) { |
|||
throw new Error("Supplied key is not an index") |
|||
} |
|||
|
|||
if (isShardedIndex(indexNode)) { |
|||
const shardKeys = await getAllShardKeys(app, indexNode, indexDir) |
|||
for (const k of shardKeys) { |
|||
await tryAwaitOrIgnore(app.datastore.deleteFile(k)) |
|||
} |
|||
tryAwaitOrIgnore(await app.datastore.deleteFile(getShardMapKey(indexDir))) |
|||
} else { |
|||
await tryAwaitOrIgnore( |
|||
app.datastore.deleteFile(getUnshardedIndexDataKey(indexDir)) |
|||
) |
|||
} |
|||
|
|||
if (includeFolder) { |
|||
tryAwaitOrIgnore(await app.datastore.deleteFolder(indexDir)) |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
import { getRecordInfo } from "../recordApi/recordInfo" |
|||
import { getParentKey, getLastPartInKey } from "../templateApi/hierarchy" |
|||
import { keySep } from "../common" |
|||
|
|||
export const getIndexDir = (hierarchy, indexKey) => { |
|||
const parentKey = getParentKey(indexKey) |
|||
|
|||
if (parentKey === "") return indexKey |
|||
if (parentKey === keySep) return indexKey |
|||
|
|||
const recordInfo = getRecordInfo(hierarchy, parentKey) |
|||
|
|||
return recordInfo.child(getLastPartInKey(indexKey)) |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
import { buildIndex } from "./buildIndex" |
|||
import { listItems } from "./listItems" |
|||
import { aggregates } from "./aggregates" |
|||
|
|||
export const getIndexApi = app => ({ |
|||
listItems: listItems(app), |
|||
buildIndex: buildIndex(app), |
|||
aggregates: aggregates(app), |
|||
}) |
|||
|
|||
export default getIndexApi |
|||
@ -1,76 +0,0 @@ |
|||
import { flatten, merge } from "lodash/fp" |
|||
import { safeKey, apiWrapper, $, events, isNonEmptyString } from "../common" |
|||
import { readIndex, searchIndex } from "../indexing/read" |
|||
import { |
|||
getUnshardedIndexDataKey, |
|||
getShardKeysInRange, |
|||
} from "../indexing/sharding" |
|||
import { |
|||
getExactNodeForKey, |
|||
isIndex, |
|||
isShardedIndex, |
|||
} from "../templateApi/hierarchy" |
|||
import { permission } from "../authApi/permissions" |
|||
import { getIndexDir } from "./getIndexDir" |
|||
|
|||
export const listItems = app => async (indexKey, options) => { |
|||
indexKey = safeKey(indexKey) |
|||
return apiWrapper( |
|||
app, |
|||
events.indexApi.listItems, |
|||
permission.readIndex.isAuthorized(indexKey), |
|||
{ indexKey, options }, |
|||
_listItems, |
|||
app, |
|||
indexKey, |
|||
options |
|||
) |
|||
} |
|||
|
|||
const defaultOptions = { |
|||
rangeStartParams: null, |
|||
rangeEndParams: null, |
|||
searchPhrase: null, |
|||
} |
|||
|
|||
export const _listItems = async (app, indexKey, options = defaultOptions) => { |
|||
const { searchPhrase, rangeStartParams, rangeEndParams } = $({}, [ |
|||
merge(options), |
|||
merge(defaultOptions), |
|||
]) |
|||
|
|||
const getItems = async indexedDataKey => |
|||
isNonEmptyString(searchPhrase) |
|||
? await searchIndex( |
|||
app.hierarchy, |
|||
app.datastore, |
|||
indexNode, |
|||
indexedDataKey, |
|||
searchPhrase |
|||
) |
|||
: await readIndex(app.hierarchy, app.datastore, indexNode, indexedDataKey) |
|||
|
|||
indexKey = safeKey(indexKey) |
|||
const indexNode = getExactNodeForKey(app.hierarchy)(indexKey) |
|||
const indexDir = getIndexDir(app.hierarchy, indexKey) |
|||
|
|||
if (!isIndex(indexNode)) { |
|||
throw new Error("supplied key is not an index") |
|||
} |
|||
|
|||
if (isShardedIndex(indexNode)) { |
|||
const shardKeys = await getShardKeysInRange( |
|||
app, |
|||
indexNode, |
|||
indexDir, |
|||
rangeStartParams, |
|||
rangeEndParams |
|||
) |
|||
const items = [] |
|||
for (const k of shardKeys) { |
|||
items.push(await getItems(k)) |
|||
} |
|||
return flatten(items) |
|||
} |
|||
return await getItems(getUnshardedIndexDataKey(indexDir)) |
|||
} |
|||
@ -1,279 +0,0 @@ |
|||
import { flatten, orderBy, filter, isUndefined } from "lodash/fp" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
getCollectionNodeByKeyOrNodeKey, |
|||
getNodeByKeyOrNodeKey, |
|||
isCollectionRecord, |
|||
isAncestor, |
|||
} from "../templateApi/hierarchy" |
|||
import { joinKey, safeKey, $ } from "../common" |
|||
import { getCollectionDir } from "../recordApi/recordInfo" |
|||
|
|||
export const RECORDS_PER_FOLDER = 1000 |
|||
export const allIdChars = |
|||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" |
|||
|
|||
// this should never be changed - ever
|
|||
// - existing databases depend on the order of chars this string
|
|||
|
|||
/** |
|||
* folderStructureArray should return an array like |
|||
* - [1] = all records fit into one folder |
|||
* - [2] = all records fite into 2 folders |
|||
* - [64, 3] = all records fit into 64 * 3 folders |
|||
* - [64, 64, 10] = all records fit into 64 * 64 * 10 folder |
|||
* (there are 64 possible chars in allIsChars) |
|||
*/ |
|||
export const folderStructureArray = recordNode => { |
|||
const totalFolders = Math.ceil(recordNode.estimatedRecordCount / 1000) |
|||
const folderArray = [] |
|||
let levelCount = 1 |
|||
while (64 ** levelCount < totalFolders) { |
|||
levelCount += 1 |
|||
folderArray.push(64) |
|||
} |
|||
|
|||
const parentFactor = 64 ** folderArray.length |
|||
if (parentFactor < totalFolders) { |
|||
folderArray.push(Math.ceil(totalFolders / parentFactor)) |
|||
} |
|||
|
|||
return folderArray |
|||
|
|||
/* |
|||
const maxRecords = currentFolderPosition === 0 |
|||
? RECORDS_PER_FOLDER |
|||
: currentFolderPosition * 64 * RECORDS_PER_FOLDER; |
|||
|
|||
if(maxRecords < recordNode.estimatedRecordCount) { |
|||
return folderStructureArray( |
|||
recordNode, |
|||
[...currentArray, 64], |
|||
currentFolderPosition + 1); |
|||
} else { |
|||
const childFolderCount = Math.ceil(recordNode.estimatedRecordCount / maxRecords ); |
|||
return [...currentArray, childFolderCount] |
|||
}*/ |
|||
} |
|||
|
|||
export const getAllIdsIterator = app => async collection_Key_or_NodeKey => { |
|||
collection_Key_or_NodeKey = safeKey(collection_Key_or_NodeKey) |
|||
const recordNode = |
|||
getCollectionNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey) || |
|||
getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey) |
|||
|
|||
const getAllIdsIteratorForCollectionKey = async ( |
|||
recordNode, |
|||
collectionKey |
|||
) => { |
|||
const folderStructure = folderStructureArray(recordNode) |
|||
|
|||
let currentFolderContents = [] |
|||
let currentPosition = [] |
|||
|
|||
const collectionDir = getCollectionDir(app.hierarchy, collectionKey) |
|||
const basePath = joinKey(collectionDir, recordNode.nodeId.toString()) |
|||
|
|||
// "folderStructure" determines the top, sharding folders
|
|||
// we need to add one, for the collection root folder, which
|
|||
// always exists
|
|||
const levels = folderStructure.length + 1 |
|||
const topLevel = levels - 1 |
|||
|
|||
/* populate initial directory structure in form: |
|||
[ |
|||
{path: "/a", contents: ["b", "c", "d"]}, |
|||
{path: "/a/b", contents: ["e","f","g"]}, |
|||
{path: "/a/b/e", contents: ["1-abcd","2-cdef","3-efgh"]}, |
|||
] |
|||
// stores contents on each parent level
|
|||
// top level has ID folders
|
|||
*/ |
|||
const firstFolder = async () => { |
|||
let folderLevel = 0 |
|||
|
|||
const lastPathHasContent = () => |
|||
folderLevel === 0 || |
|||
currentFolderContents[folderLevel - 1].contents.length > 0 |
|||
|
|||
while (folderLevel <= topLevel && lastPathHasContent()) { |
|||
let thisPath = basePath |
|||
for (let lev = 0; lev < currentPosition.length; lev++) { |
|||
thisPath = joinKey(thisPath, currentFolderContents[lev].contents[0]) |
|||
} |
|||
|
|||
const contentsThisLevel = await app.datastore.getFolderContents( |
|||
thisPath |
|||
) |
|||
currentFolderContents.push({ |
|||
contents: contentsThisLevel, |
|||
path: thisPath, |
|||
}) |
|||
|
|||
// should start as something like [0,0]
|
|||
if (folderLevel < topLevel) currentPosition.push(0) |
|||
|
|||
folderLevel += 1 |
|||
} |
|||
|
|||
return currentPosition.length === levels - 1 |
|||
} |
|||
|
|||
const isOnLastFolder = level => { |
|||
const result = |
|||
currentPosition[level] === |
|||
currentFolderContents[level].contents.length - 1 |
|||
return result |
|||
} |
|||
|
|||
const getNextFolder = async (lev = undefined) => { |
|||
lev = isUndefined(lev) ? topLevel : lev |
|||
const parentLev = lev - 1 |
|||
|
|||
if (parentLev < 0) return false |
|||
|
|||
if (isOnLastFolder(parentLev)) { |
|||
return await getNextFolder(parentLev) |
|||
} |
|||
|
|||
const newPosition = currentPosition[parentLev] + 1 |
|||
currentPosition[parentLev] = newPosition |
|||
|
|||
const nextFolder = joinKey( |
|||
currentFolderContents[parentLev].path, |
|||
currentFolderContents[parentLev].contents[newPosition] |
|||
) |
|||
currentFolderContents[ |
|||
lev |
|||
].contents = await app.datastore.getFolderContents(nextFolder) |
|||
currentFolderContents[lev].path = nextFolder |
|||
|
|||
if (lev !== topLevel) { |
|||
// we just advanced a parent folder, so now need to
|
|||
// do the same to the next levels
|
|||
let loopLevel = lev + 1 |
|||
while (loopLevel <= topLevel) { |
|||
const loopParentLevel = loopLevel - 1 |
|||
|
|||
currentPosition[loopParentLevel] = 0 |
|||
const nextLoopFolder = joinKey( |
|||
currentFolderContents[loopParentLevel].path, |
|||
currentFolderContents[loopParentLevel].contents[0] |
|||
) |
|||
currentFolderContents[ |
|||
loopLevel |
|||
].contents = await app.datastore.getFolderContents(nextLoopFolder) |
|||
currentFolderContents[loopLevel].path = nextLoopFolder |
|||
loopLevel += 1 |
|||
} |
|||
} |
|||
|
|||
// true ==has more ids... (just loaded more)
|
|||
return true |
|||
} |
|||
|
|||
const idsCurrentFolder = () => |
|||
currentFolderContents[currentFolderContents.length - 1].contents |
|||
|
|||
const fininshedResult = { done: true, result: { ids: [], collectionKey } } |
|||
|
|||
let hasStarted = false |
|||
let hasMore = true |
|||
const getIdsFromCurrentfolder = async () => { |
|||
if (!hasMore) { |
|||
return fininshedResult |
|||
} |
|||
|
|||
if (!hasStarted) { |
|||
hasMore = await firstFolder() |
|||
hasStarted = true |
|||
return { |
|||
result: { |
|||
ids: idsCurrentFolder(), |
|||
collectionKey, |
|||
}, |
|||
done: false, |
|||
} |
|||
} |
|||
|
|||
hasMore = await getNextFolder() |
|||
|
|||
return { |
|||
result: { |
|||
ids: hasMore ? idsCurrentFolder() : [], |
|||
collectionKey, |
|||
}, |
|||
done: !hasMore, |
|||
} |
|||
} |
|||
|
|||
return getIdsFromCurrentfolder |
|||
} |
|||
|
|||
const ancestors = $(getFlattenedHierarchy(app.hierarchy), [ |
|||
filter(isCollectionRecord), |
|||
filter( |
|||
n => isAncestor(recordNode)(n) || n.nodeKey() === recordNode.nodeKey() |
|||
), |
|||
orderBy([n => n.nodeKey().length], ["asc"]), |
|||
]) // parents first
|
|||
|
|||
const traverseForIteraterators = async ( |
|||
parentRecordKey = "", |
|||
currentNodeIndex = 0 |
|||
) => { |
|||
const currentNode = ancestors[currentNodeIndex] |
|||
const currentCollectionKey = joinKey( |
|||
parentRecordKey, |
|||
currentNode.collectionName |
|||
) |
|||
if (currentNode.nodeKey() === recordNode.nodeKey()) { |
|||
return [ |
|||
await getAllIdsIteratorForCollectionKey( |
|||
currentNode, |
|||
currentCollectionKey |
|||
), |
|||
] |
|||
} |
|||
const allIterators = [] |
|||
const currentIterator = await getAllIdsIteratorForCollectionKey( |
|||
currentNode, |
|||
currentCollectionKey |
|||
) |
|||
|
|||
let ids = await currentIterator() |
|||
while (ids.done === false) { |
|||
for (const id of ids.result.ids) { |
|||
allIterators.push( |
|||
await traverseForIteraterators( |
|||
joinKey(currentCollectionKey, id), |
|||
currentNodeIndex + 1 |
|||
) |
|||
) |
|||
} |
|||
|
|||
ids = await currentIterator() |
|||
} |
|||
|
|||
return flatten(allIterators) |
|||
} |
|||
|
|||
const iteratorsArray = await traverseForIteraterators() |
|||
let currentIteratorIndex = 0 |
|||
return async () => { |
|||
if (iteratorsArray.length === 0) { |
|||
return { done: true, result: [] } |
|||
} |
|||
const innerResult = await iteratorsArray[currentIteratorIndex]() |
|||
if (!innerResult.done) { |
|||
return innerResult |
|||
} |
|||
if (currentIteratorIndex == iteratorsArray.length - 1) { |
|||
return { done: true, result: innerResult.result } |
|||
} |
|||
currentIteratorIndex++ |
|||
return { done: false, result: innerResult.result } |
|||
} |
|||
} |
|||
|
|||
export default getAllIdsIterator |
|||
@ -1,107 +0,0 @@ |
|||
import { ensureShardNameIsInShardMap } from "./sharding" |
|||
import { getIndexWriter } from "./serializer" |
|||
import { isShardedIndex, getParentKey } from "../templateApi/hierarchy" |
|||
import { promiseWriteableStream } from "./promiseWritableStream" |
|||
import { promiseReadableStream } from "./promiseReadableStream" |
|||
|
|||
export const applyToShard = async ( |
|||
hierarchy, |
|||
store, |
|||
indexDir, |
|||
indexNode, |
|||
indexShardKey, |
|||
recordsToWrite, |
|||
keysToRemove |
|||
) => { |
|||
const createIfNotExists = recordsToWrite.length > 0 |
|||
const writer = await getWriter( |
|||
hierarchy, |
|||
store, |
|||
indexDir, |
|||
indexShardKey, |
|||
indexNode, |
|||
createIfNotExists |
|||
) |
|||
if (writer === SHARD_DELETED) return |
|||
|
|||
await writer.updateIndex(recordsToWrite, keysToRemove) |
|||
await swapTempFileIn(store, indexShardKey) |
|||
} |
|||
|
|||
const SHARD_DELETED = "SHARD_DELETED" |
|||
const getWriter = async ( |
|||
hierarchy, |
|||
store, |
|||
indexDir, |
|||
indexedDataKey, |
|||
indexNode, |
|||
createIfNotExists |
|||
) => { |
|||
let readableStream = null |
|||
|
|||
if (isShardedIndex(indexNode)) { |
|||
await ensureShardNameIsInShardMap(store, indexDir, indexedDataKey) |
|||
if (!(await store.exists(indexedDataKey))) { |
|||
if (await store.exists(getParentKey(indexedDataKey))) { |
|||
await store.createFile(indexedDataKey, "") |
|||
} else { |
|||
return SHARD_DELETED |
|||
} |
|||
} |
|||
} |
|||
|
|||
try { |
|||
readableStream = promiseReadableStream( |
|||
await store.readableFileStream(indexedDataKey) |
|||
) |
|||
} catch (e) { |
|||
if (await store.exists(indexedDataKey)) { |
|||
throw e |
|||
} else { |
|||
if (createIfNotExists) { |
|||
if (await store.exists(getParentKey(indexedDataKey))) { |
|||
await store.createFile(indexedDataKey, "") |
|||
} else { |
|||
return SHARD_DELETED |
|||
} |
|||
} else { |
|||
return SHARD_DELETED |
|||
} |
|||
|
|||
readableStream = promiseReadableStream( |
|||
await store.readableFileStream(indexedDataKey) |
|||
) |
|||
} |
|||
} |
|||
|
|||
const writableStream = promiseWriteableStream( |
|||
await store.writableFileStream(indexedDataKey + ".temp") |
|||
) |
|||
|
|||
return getIndexWriter(hierarchy, indexNode, readableStream, writableStream) |
|||
} |
|||
|
|||
const swapTempFileIn = async (store, indexedDataKey, isRetry = false) => { |
|||
const tempFile = `${indexedDataKey}.temp` |
|||
try { |
|||
await store.deleteFile(indexedDataKey) |
|||
} catch (e) { |
|||
// ignore failure, incase it has not been created yet
|
|||
|
|||
// if parent folder does not exist, assume that this index
|
|||
// should not be there
|
|||
if (!(await store.exists(getParentKey(indexedDataKey)))) { |
|||
return |
|||
} |
|||
} |
|||
try { |
|||
await store.renameFile(tempFile, indexedDataKey) |
|||
} catch (e) { |
|||
// retrying in case delete failure was for some other reason
|
|||
if (!isRetry) { |
|||
await swapTempFileIn(store, indexedDataKey, true) |
|||
} else { |
|||
throw new Error("Failed to swap in index filed: " + e.message) |
|||
} |
|||
} |
|||
} |
|||
@ -1,85 +0,0 @@ |
|||
import { compileCode } from "../common/compileCode" |
|||
import { isUndefined, keys, cloneDeep, isFunction, includes } from "lodash/fp" |
|||
import { defineError } from "../common" |
|||
|
|||
export const filterEval = "FILTER_EVALUATE" |
|||
export const filterCompile = "FILTER_COMPILE" |
|||
export const mapEval = "MAP_EVALUATE" |
|||
export const mapCompile = "MAP_COMPILE" |
|||
export const removeUndeclaredFields = "REMOVE_UNDECLARED_FIELDS" |
|||
export const addUnMappedFields = "ADD_UNMAPPED_FIELDS" |
|||
export const addTheKey = "ADD_KEY" |
|||
|
|||
const getEvaluateResult = () => ({ |
|||
isError: false, |
|||
passedFilter: true, |
|||
result: null, |
|||
}) |
|||
|
|||
export const compileFilter = index => compileCode(index.filter) |
|||
|
|||
export const compileMap = index => compileCode(index.map) |
|||
|
|||
export const passesFilter = (record, index) => { |
|||
const context = { record } |
|||
if (!index.filter) return true |
|||
|
|||
const compiledFilter = defineError(() => compileFilter(index), filterCompile) |
|||
|
|||
return defineError(() => compiledFilter(context), filterEval) |
|||
} |
|||
|
|||
export const mapRecord = (record, index) => { |
|||
const recordClone = cloneDeep(record) |
|||
const context = { record: recordClone } |
|||
|
|||
const map = index.map ? index.map : "return {...record};" |
|||
|
|||
const compiledMap = defineError(() => compileCode(map), mapCompile) |
|||
|
|||
const mapped = defineError(() => compiledMap(context), mapEval) |
|||
|
|||
const mappedKeys = keys(mapped) |
|||
for (let i = 0; i < mappedKeys.length; i++) { |
|||
const key = mappedKeys[i] |
|||
mapped[key] = isUndefined(mapped[key]) ? null : mapped[key] |
|||
if (isFunction(mapped[key])) { |
|||
delete mapped[key] |
|||
} |
|||
if (key === "IsNew") { |
|||
delete mapped.IsNew |
|||
} |
|||
} |
|||
|
|||
mapped.key = record.key |
|||
mapped.sortKey = index.getSortKey |
|||
? compileCode(index.getSortKey)(context) |
|||
: record.id |
|||
|
|||
return mapped |
|||
} |
|||
|
|||
export const evaluate = record => index => { |
|||
const result = getEvaluateResult() |
|||
|
|||
try { |
|||
result.passedFilter = passesFilter(record, index) |
|||
} catch (err) { |
|||
result.isError = true |
|||
result.passedFilter = false |
|||
result.result = err.message |
|||
} |
|||
|
|||
if (!result.passedFilter) return result |
|||
|
|||
try { |
|||
result.result = mapRecord(record, index) |
|||
} catch (err) { |
|||
result.isError = true |
|||
result.result = err.message |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
export default evaluate |
|||
@ -1,58 +0,0 @@ |
|||
import { has, keys, map, orderBy, filter, concat, reverse } from "lodash/fp" |
|||
import { getAllowedRecordNodesForIndex } from "../templateApi/hierarchy" |
|||
import { mapRecord } from "./evaluate" |
|||
import { constructRecord } from "../recordApi/getNew" |
|||
import { getSampleFieldValue, detectType, all } from "../types" |
|||
import { $ } from "../common" |
|||
|
|||
export const generateSchema = (hierarchy, indexNode) => { |
|||
const recordNodes = getAllowedRecordNodesForIndex(hierarchy, indexNode) |
|||
const mappedRecords = $(recordNodes, [ |
|||
map(n => mapRecord(createSampleRecord(n), indexNode)), |
|||
]) |
|||
|
|||
// always has record key and sort key
|
|||
const schema = { |
|||
sortKey: all.string, |
|||
key: all.string, |
|||
} |
|||
|
|||
const fieldsHas = has(schema) |
|||
const setField = (fieldName, value) => { |
|||
if (value === null || value === undefined) { |
|||
return |
|||
} |
|||
|
|||
const thisType = detectType(value) |
|||
if (fieldsHas(fieldName)) { |
|||
if (schema[fieldName] !== thisType) { |
|||
schema[fieldName] = all.string |
|||
} |
|||
} else { |
|||
schema[fieldName] = thisType |
|||
} |
|||
} |
|||
|
|||
for (const mappedRec of mappedRecords) { |
|||
for (const f in mappedRec) { |
|||
setField(f, mappedRec[f]) |
|||
} |
|||
} |
|||
|
|||
// returing an array of {name, type}
|
|||
return $(schema, [ |
|||
keys, |
|||
map(k => ({ name: k, type: schema[k].name })), |
|||
filter(s => s.name !== "sortKey"), |
|||
orderBy("name", ["desc"]), // reverse alpha
|
|||
concat([{ name: "sortKey", type: all.string.name }]), // sortKey on end
|
|||
reverse, // sortKey first, then rest are alphabetical
|
|||
]) |
|||
} |
|||
|
|||
const createSampleRecord = recordNode => |
|||
constructRecord( |
|||
recordNode, |
|||
getSampleFieldValue, |
|||
recordNode.parent().nodeKey() |
|||
) |
|||
@ -1,27 +0,0 @@ |
|||
import { isShardedIndex } from "../templateApi/hierarchy" |
|||
import { joinKey } from "../common" |
|||
import { |
|||
getShardMapKey, |
|||
getUnshardedIndexDataKey, |
|||
createIndexFile, |
|||
} from "./sharding" |
|||
|
|||
export const initialiseIndex = async (datastore, dir, index) => { |
|||
const indexDir = joinKey(dir, index.name) |
|||
|
|||
let newDir = false |
|||
if (!(await datastore.exists(indexDir))) { |
|||
await datastore.createFolder(indexDir) |
|||
newDir = true |
|||
} |
|||
|
|||
if (isShardedIndex(index)) { |
|||
const shardFile = getShardMapKey(indexDir) |
|||
if (newDir || !(await datastore.exists(shardFile))) |
|||
await datastore.createFile(shardFile, "[]") |
|||
} else { |
|||
const indexFile = getUnshardedIndexDataKey(indexDir) |
|||
if (newDir || !(await datastore.exists(indexFile))) |
|||
await createIndexFile(datastore, indexFile, index) |
|||
} |
|||
} |
|||
@ -1,80 +0,0 @@ |
|||
// adapted from https://github.com/dex4er/js-promise-readable
|
|||
// thanks :)
|
|||
|
|||
export const promiseReadableStream = stream => { |
|||
let _errored |
|||
|
|||
const _errorHandler = err => { |
|||
_errored = err |
|||
} |
|||
|
|||
stream.on("error", _errorHandler) |
|||
|
|||
const read = size => { |
|||
return new Promise((resolve, reject) => { |
|||
if (_errored) { |
|||
const err = _errored |
|||
_errored = undefined |
|||
return reject(err) |
|||
} |
|||
|
|||
if (!stream.readable || stream.closed || stream.destroyed) { |
|||
return resolve() |
|||
} |
|||
|
|||
const readableHandler = () => { |
|||
const chunk = stream.read(size) |
|||
|
|||
if (chunk) { |
|||
removeListeners() |
|||
resolve(chunk) |
|||
} |
|||
} |
|||
|
|||
const closeHandler = () => { |
|||
removeListeners() |
|||
resolve() |
|||
} |
|||
|
|||
const endHandler = () => { |
|||
removeListeners() |
|||
resolve() |
|||
} |
|||
|
|||
const errorHandler = err => { |
|||
_errored = undefined |
|||
removeListeners() |
|||
reject(err) |
|||
} |
|||
|
|||
const removeListeners = () => { |
|||
stream.removeListener("close", closeHandler) |
|||
stream.removeListener("error", errorHandler) |
|||
stream.removeListener("end", endHandler) |
|||
stream.removeListener("readable", readableHandler) |
|||
} |
|||
|
|||
stream.on("close", closeHandler) |
|||
stream.on("end", endHandler) |
|||
stream.on("error", errorHandler) |
|||
stream.on("readable", readableHandler) |
|||
|
|||
readableHandler() |
|||
}) |
|||
} |
|||
|
|||
const destroy = () => { |
|||
if (stream) { |
|||
if (_errorHandler) { |
|||
stream.removeListener("error", _errorHandler) |
|||
} |
|||
if (typeof stream.destroy === "function") { |
|||
stream.destroy() |
|||
} |
|||
} |
|||
} |
|||
|
|||
return { read, destroy, stream } |
|||
} |
|||
|
|||
export default promiseReadableStream |
|||
@ -1,117 +0,0 @@ |
|||
// adapted from https://github.com/dex4er/js-promise-writable
|
|||
// Thank you :)
|
|||
export const promiseWriteableStream = stream => { |
|||
let _errored |
|||
|
|||
const _errorHandler = err => { |
|||
_errored = err |
|||
} |
|||
|
|||
stream.on("error", _errorHandler) |
|||
|
|||
const write = chunk => { |
|||
let rejected = false |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
if (_errored) { |
|||
const err = _errored |
|||
_errored = undefined |
|||
return reject(err) |
|||
} |
|||
|
|||
if (!stream.writable || stream.closed || stream.destroyed) { |
|||
return reject(new Error("write after end")) |
|||
} |
|||
|
|||
const writeErrorHandler = err => { |
|||
_errored = undefined |
|||
rejected = true |
|||
reject(err) |
|||
} |
|||
|
|||
stream.once("error", writeErrorHandler) |
|||
|
|||
const canWrite = stream.write(chunk) |
|||
|
|||
stream.removeListener("error", writeErrorHandler) |
|||
|
|||
if (canWrite) { |
|||
if (!rejected) { |
|||
resolve(chunk.length) |
|||
} |
|||
} else { |
|||
const errorHandler = err => { |
|||
_errored = undefined |
|||
removeListeners() |
|||
reject(err) |
|||
} |
|||
|
|||
const drainHandler = () => { |
|||
removeListeners() |
|||
resolve(chunk.length) |
|||
} |
|||
|
|||
const closeHandler = () => { |
|||
removeListeners() |
|||
resolve(chunk.length) |
|||
} |
|||
|
|||
const finishHandler = () => { |
|||
removeListeners() |
|||
resolve(chunk.length) |
|||
} |
|||
|
|||
const removeListeners = () => { |
|||
stream.removeListener("close", closeHandler) |
|||
stream.removeListener("drain", drainHandler) |
|||
stream.removeListener("error", errorHandler) |
|||
stream.removeListener("finish", finishHandler) |
|||
} |
|||
|
|||
stream.on("close", closeHandler) |
|||
stream.on("drain", drainHandler) |
|||
stream.on("error", errorHandler) |
|||
stream.on("finish", finishHandler) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const end = () => { |
|||
return new Promise((resolve, reject) => { |
|||
if (_errored) { |
|||
const err = _errored |
|||
_errored = undefined |
|||
return reject(err) |
|||
} |
|||
|
|||
if (!stream.writable || stream.closed || stream.destroyed) { |
|||
return resolve() |
|||
} |
|||
|
|||
const finishHandler = () => { |
|||
removeListeners() |
|||
resolve() |
|||
} |
|||
|
|||
const errorHandler = err => { |
|||
_errored = undefined |
|||
removeListeners() |
|||
reject(err) |
|||
} |
|||
|
|||
const removeListeners = () => { |
|||
stream.removeListener("error", errorHandler) |
|||
stream.removeListener("finish", finishHandler) |
|||
} |
|||
|
|||
stream.on("finish", finishHandler) |
|||
stream.on("error", errorHandler) |
|||
|
|||
stream.end() |
|||
}) |
|||
} |
|||
|
|||
return { write, end } |
|||
} |
|||
|
|||
export default promiseWriteableStream |
|||
@ -1,93 +0,0 @@ |
|||
import lunr from "lunr" |
|||
import { promiseReadableStream } from "./promiseReadableStream" |
|||
import { createIndexFile } from "./sharding" |
|||
import { generateSchema } from "./indexSchemaCreator" |
|||
import { getIndexReader, CONTINUE_READING_RECORDS } from "./serializer" |
|||
import { |
|||
getAllowedRecordNodesForIndex, |
|||
getRecordNodeId, |
|||
} from "../templateApi/hierarchy" |
|||
import { $ } from "../common" |
|||
import { filter, includes, find } from "lodash/fp" |
|||
|
|||
export const readIndex = async ( |
|||
hierarchy, |
|||
datastore, |
|||
index, |
|||
indexedDataKey |
|||
) => { |
|||
const records = [] |
|||
const getType = typeLoader(index, hierarchy) |
|||
const doRead = iterateIndex( |
|||
async item => { |
|||
item.type = getType(item.key) |
|||
records.push(item) |
|||
return CONTINUE_READING_RECORDS |
|||
}, |
|||
async () => records |
|||
) |
|||
|
|||
return await doRead(hierarchy, datastore, index, indexedDataKey) |
|||
} |
|||
|
|||
export const searchIndex = async ( |
|||
hierarchy, |
|||
datastore, |
|||
index, |
|||
indexedDataKey, |
|||
searchPhrase |
|||
) => { |
|||
const records = [] |
|||
const schema = generateSchema(hierarchy, index) |
|||
const getType = typeLoader(index, hierarchy) |
|||
const doRead = iterateIndex( |
|||
async item => { |
|||
item.type = getType(item.key) |
|||
const idx = lunr(function() { |
|||
this.ref("key") |
|||
for (const field of schema) { |
|||
this.field(field.name) |
|||
} |
|||
this.add(item) |
|||
}) |
|||
const searchResults = idx.search(searchPhrase) |
|||
if (searchResults.length === 1) { |
|||
item._searchResult = searchResults[0] |
|||
records.push(item) |
|||
} |
|||
return CONTINUE_READING_RECORDS |
|||
}, |
|||
async () => records |
|||
) |
|||
|
|||
return await doRead(hierarchy, datastore, index, indexedDataKey) |
|||
} |
|||
|
|||
export const iterateIndex = (onGetItem, getFinalResult) => async ( |
|||
hierarchy, |
|||
datastore, |
|||
index, |
|||
indexedDataKey |
|||
) => { |
|||
try { |
|||
const readableStream = promiseReadableStream( |
|||
await datastore.readableFileStream(indexedDataKey) |
|||
) |
|||
|
|||
const read = getIndexReader(hierarchy, index, readableStream) |
|||
await read(onGetItem) |
|||
return getFinalResult() |
|||
} catch (e) { |
|||
if (await datastore.exists(indexedDataKey)) { |
|||
throw e |
|||
} else { |
|||
await createIndexFile(datastore, indexedDataKey, index) |
|||
} |
|||
return [] |
|||
} |
|||
} |
|||
|
|||
const typeLoader = (index, hierarchy) => { |
|||
const allowedNodes = getAllowedRecordNodesForIndex(hierarchy, index) |
|||
return key => find(n => getRecordNodeId(key) === n.nodeId)(allowedNodes).name |
|||
} |
|||
@ -1,126 +0,0 @@ |
|||
import { orderBy } from "lodash" |
|||
import { |
|||
reduce, |
|||
find, |
|||
includes, |
|||
flatten, |
|||
union, |
|||
filter, |
|||
each, |
|||
map, |
|||
} from "lodash/fp" |
|||
import { |
|||
joinKey, |
|||
splitKey, |
|||
isNonEmptyString, |
|||
isNothing, |
|||
$, |
|||
isSomething, |
|||
} from "../common" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
getNode, |
|||
getRecordNodeId, |
|||
getExactNodeForKey, |
|||
recordNodeIdIsAllowed, |
|||
isModel, |
|||
isGlobalIndex, |
|||
} from "../templateApi/hierarchy" |
|||
import { indexTypes } from "../templateApi/indexes" |
|||
import { getIndexDir } from "../indexApi/getIndexDir" |
|||
import { getRecordInfo } from "../recordApi/recordInfo" |
|||
|
|||
export const getRelevantAncestorIndexes = (hierarchy, record) => { |
|||
const key = record.key |
|||
const keyParts = splitKey(key) |
|||
const nodeId = getRecordNodeId(key) |
|||
|
|||
const flatHierarchy = orderBy( |
|||
getFlattenedHierarchy(hierarchy), |
|||
[node => node.pathRegx().length], |
|||
["desc"] |
|||
) |
|||
|
|||
const makeindexNodeAndDir_ForAncestorIndex = (indexNode, parentRecordDir) => |
|||
makeIndexNodeAndDir(indexNode, joinKey(parentRecordDir, indexNode.name)) |
|||
|
|||
const traverseAncestorIndexesInPath = () => |
|||
reduce( |
|||
(acc, part) => { |
|||
const currentIndexKey = joinKey(acc.lastIndexKey, part) |
|||
acc.lastIndexKey = currentIndexKey |
|||
const testPathRegx = p => |
|||
new RegExp(`${p.pathRegx()}$`).test(currentIndexKey) |
|||
const nodeMatch = find(testPathRegx)(flatHierarchy) |
|||
|
|||
if (isNothing(nodeMatch)) { |
|||
return acc |
|||
} |
|||
|
|||
if (!isModel(nodeMatch) || nodeMatch.indexes.length === 0) { |
|||
return acc |
|||
} |
|||
|
|||
const indexes = $(nodeMatch.indexes, [ |
|||
filter( |
|||
i => |
|||
i.indexType === indexTypes.ancestor && |
|||
(i.allowedModelNodeIds.length === 0 || |
|||
includes(nodeId)(i.allowedModelNodeIds)) |
|||
), |
|||
]) |
|||
|
|||
const currentRecordDir = getRecordInfo(hierarchy, currentIndexKey).dir |
|||
|
|||
each(v => |
|||
acc.nodesAndKeys.push( |
|||
makeindexNodeAndDir_ForAncestorIndex(v, currentRecordDir) |
|||
) |
|||
)(indexes) |
|||
|
|||
return acc |
|||
}, |
|||
{ lastIndexKey: "", nodesAndKeys: [] } |
|||
)(keyParts).nodesAndKeys |
|||
|
|||
const rootIndexes = $(flatHierarchy, [ |
|||
filter(n => isGlobalIndex(n) && recordNodeIdIsAllowed(n)(nodeId)), |
|||
map(i => makeIndexNodeAndDir(i, getIndexDir(hierarchy, i.nodeKey()))), |
|||
]) |
|||
|
|||
return union(traverseAncestorIndexesInPath())(rootIndexes) |
|||
} |
|||
|
|||
export const getRelevantReverseReferenceIndexes = (hierarchy, record) => |
|||
$(record.key, [ |
|||
getExactNodeForKey(hierarchy), |
|||
n => n.fields, |
|||
filter( |
|||
f => |
|||
f.type === "reference" && |
|||
isSomething(record[f.name]) && |
|||
isNonEmptyString(record[f.name].key) |
|||
), |
|||
map(f => |
|||
$(f.typeOptions.reverseIndexNodeKeys, [ |
|||
map(n => ({ |
|||
recordNode: getNode(hierarchy, n), |
|||
field: f, |
|||
})), |
|||
]) |
|||
), |
|||
flatten, |
|||
map(n => |
|||
makeIndexNodeAndDir( |
|||
n.recordNode, |
|||
joinKey( |
|||
getRecordInfo(hierarchy, record[n.field.name].key).dir, |
|||
n.recordNode.name |
|||
) |
|||
) |
|||
), |
|||
]) |
|||
|
|||
const makeIndexNodeAndDir = (indexNode, indexDir) => ({ indexNode, indexDir }) |
|||
|
|||
export default getRelevantAncestorIndexes |
|||
@ -1,238 +0,0 @@ |
|||
import { generateSchema } from "./indexSchemaCreator" |
|||
import { has, isString, difference, find } from "lodash/fp" |
|||
import { Buffer } from "safe-buffer" |
|||
import { StringDecoder } from "string_decoder" |
|||
import { getType } from "../types" |
|||
import { isSomething } from "../common" |
|||
|
|||
export const BUFFER_MAX_BYTES = 524288 // 0.5Mb
|
|||
|
|||
export const CONTINUE_READING_RECORDS = "CONTINUE_READING" |
|||
export const READ_REMAINING_TEXT = "READ_REMAINING" |
|||
export const CANCEL_READ = "CANCEL" |
|||
|
|||
export const getIndexWriter = ( |
|||
hierarchy, |
|||
indexNode, |
|||
readableStream, |
|||
writableStream, |
|||
end |
|||
) => { |
|||
const schema = generateSchema(hierarchy, indexNode) |
|||
|
|||
return { |
|||
read: read(readableStream, schema), |
|||
updateIndex: updateIndex(readableStream, writableStream, schema, end), |
|||
} |
|||
} |
|||
|
|||
export const getIndexReader = (hierarchy, indexNode, readableStream) => |
|||
read(readableStream, generateSchema(hierarchy, indexNode)) |
|||
|
|||
const updateIndex = (readableStream, writableStream, schema) => async ( |
|||
itemsToWrite, |
|||
keysToRemove |
|||
) => { |
|||
const write = newOutputWriter(BUFFER_MAX_BYTES, writableStream) |
|||
const writtenItems = [] |
|||
await read(readableStream, schema)( |
|||
async indexedItem => { |
|||
const updated = find(i => indexedItem.key === i.key)(itemsToWrite) |
|||
const removed = find(k => indexedItem.key === k)(keysToRemove) |
|||
|
|||
if (isSomething(removed)) return CONTINUE_READING_RECORDS |
|||
|
|||
if (isSomething(updated)) { |
|||
const serializedItem = serializeItem(schema, updated) |
|||
await write(serializedItem) |
|||
writtenItems.push(updated) |
|||
} else { |
|||
await write(serializeItem(schema, indexedItem)) |
|||
} |
|||
|
|||
return CONTINUE_READING_RECORDS |
|||
}, |
|||
async text => await write(text) |
|||
) |
|||
|
|||
if (writtenItems.length !== itemsToWrite.length) { |
|||
const toAdd = difference(itemsToWrite, writtenItems) |
|||
for (let added of toAdd) { |
|||
await write(serializeItem(schema, added)) |
|||
} |
|||
} else if (writtenItems.length === 0) { |
|||
// potentially are no records
|
|||
await write("") |
|||
} |
|||
|
|||
await write() |
|||
await writableStream.end() |
|||
} |
|||
|
|||
const read = (readableStream, schema) => async (onGetItem, onGetText) => { |
|||
const readInput = newInputReader(readableStream) |
|||
let text = await readInput() |
|||
let status = CONTINUE_READING_RECORDS |
|||
while (text.length > 0) { |
|||
if (status === READ_REMAINING_TEXT) { |
|||
await onGetText(text) |
|||
continue |
|||
} |
|||
|
|||
if (status === CANCEL_READ) { |
|||
return |
|||
} |
|||
|
|||
let rowText = "" |
|||
let currentCharIndex = 0 |
|||
for (let currentChar of text) { |
|||
rowText += currentChar |
|||
if (currentChar === "\r") { |
|||
status = await onGetItem(deserializeRow(schema, rowText)) |
|||
rowText = "" |
|||
if (status === READ_REMAINING_TEXT) { |
|||
break |
|||
} |
|||
} |
|||
currentCharIndex++ |
|||
} |
|||
|
|||
if (currentCharIndex < text.length - 1) { |
|||
await onGetText(text.substring(currentCharIndex + 1)) |
|||
} |
|||
|
|||
text = await readInput() |
|||
} |
|||
|
|||
await readableStream.destroy() |
|||
} |
|||
|
|||
const newOutputWriter = (flushBoundary, writableStream) => { |
|||
let currentBuffer = null |
|||
|
|||
return async text => { |
|||
if (isString(text) && currentBuffer === null) |
|||
currentBuffer = Buffer.from(text, "utf8") |
|||
else if (isString(text)) |
|||
currentBuffer = Buffer.concat([currentBuffer, Buffer.from(text, "utf8")]) |
|||
|
|||
if ( |
|||
currentBuffer !== null && |
|||
(currentBuffer.length > flushBoundary || !isString(text)) |
|||
) { |
|||
await writableStream.write(currentBuffer) |
|||
currentBuffer = null |
|||
} |
|||
} |
|||
} |
|||
|
|||
const newInputReader = readableStream => { |
|||
const decoder = new StringDecoder("utf8") |
|||
let remainingBytes = [] |
|||
|
|||
return async () => { |
|||
let nextBytesBuffer = await readableStream.read(BUFFER_MAX_BYTES) |
|||
const remainingBuffer = Buffer.from(remainingBytes) |
|||
|
|||
if (!nextBytesBuffer) nextBytesBuffer = Buffer.from([]) |
|||
|
|||
const moreToRead = nextBytesBuffer.length === BUFFER_MAX_BYTES |
|||
|
|||
const buffer = Buffer.concat( |
|||
[remainingBuffer, nextBytesBuffer], |
|||
remainingBuffer.length + nextBytesBuffer.length |
|||
) |
|||
|
|||
const text = decoder.write(buffer) |
|||
remainingBytes = decoder.end(buffer) |
|||
|
|||
if (!moreToRead && remainingBytes.length > 0) { |
|||
// if for any reason, we have remaining bytes at the end
|
|||
// of the stream, just discard - dont see why this should
|
|||
// ever happen, but if it does, it could cause a stack overflow
|
|||
remainingBytes = [] |
|||
} |
|||
|
|||
return text |
|||
} |
|||
} |
|||
|
|||
const deserializeRow = (schema, rowText) => { |
|||
let currentPropIndex = 0 |
|||
let currentCharIndex = 0 |
|||
let currentValueText = "" |
|||
let isEscaped = false |
|||
const item = {} |
|||
|
|||
const setCurrentProp = () => { |
|||
const currentProp = schema[currentPropIndex] |
|||
const type = getType(currentProp.type) |
|||
const value = |
|||
currentValueText === "" |
|||
? type.getDefaultValue() |
|||
: type.safeParseValue(currentValueText) |
|||
item[currentProp.name] = value |
|||
} |
|||
|
|||
while (currentPropIndex < schema.length) { |
|||
if (currentCharIndex < rowText.length) { |
|||
const currentChar = rowText[currentCharIndex] |
|||
if (isEscaped) { |
|||
if (currentChar === "r") { |
|||
currentValueText += "\r" |
|||
} else { |
|||
currentValueText += currentChar |
|||
} |
|||
isEscaped = false |
|||
} else { |
|||
if (currentChar === ",") { |
|||
setCurrentProp() |
|||
currentValueText = "" |
|||
currentPropIndex++ |
|||
} else if (currentChar === "\\") { |
|||
isEscaped = true |
|||
} else { |
|||
currentValueText += currentChar |
|||
} |
|||
} |
|||
currentCharIndex++ |
|||
} else { |
|||
currentValueText = "" |
|||
setCurrentProp() |
|||
currentPropIndex++ |
|||
} |
|||
} |
|||
|
|||
return item |
|||
} |
|||
|
|||
export const serializeItem = (schema, item) => { |
|||
let rowText = "" |
|||
|
|||
for (let prop of schema) { |
|||
const type = getType(prop.type) |
|||
const value = has(prop.name)(item) |
|||
? item[prop.name] |
|||
: type.getDefaultValue() |
|||
|
|||
const valStr = type.stringify(value) |
|||
|
|||
for (let i = 0; i < valStr.length; i++) { |
|||
const currentChar = valStr[i] |
|||
if (currentChar === "," || currentChar === "\r" || currentChar === "\\") { |
|||
rowText += "\\" |
|||
} |
|||
|
|||
if (currentChar === "\r") { |
|||
rowText += "r" |
|||
} else { |
|||
rowText += currentChar |
|||
} |
|||
} |
|||
|
|||
rowText += "," |
|||
} |
|||
|
|||
rowText += "\r" |
|||
return rowText |
|||
} |
|||
@ -1,116 +0,0 @@ |
|||
import { compileCode } from "../common/compileCode" |
|||
import { filter, includes, map, last } from "lodash/fp" |
|||
import { |
|||
getActualKeyOfParent, |
|||
isGlobalIndex, |
|||
getParentKey, |
|||
isShardedIndex, |
|||
} from "../templateApi/hierarchy" |
|||
import { joinKey, isNonEmptyString, splitKey, $ } from "../common" |
|||
|
|||
export const getIndexedDataKey = (indexNode, indexDir, record) => { |
|||
const getShardName = (indexNode, record) => { |
|||
const shardNameFunc = compileCode(indexNode.getShardName) |
|||
try { |
|||
return shardNameFunc({ record }) |
|||
} catch (e) { |
|||
const errorDetails = `shardCode: ${ |
|||
indexNode.getShardName |
|||
} :: record: ${JSON.stringify(record)} :: ` |
|||
e.message = |
|||
"Error running index shardname func: " + errorDetails + e.message |
|||
throw e |
|||
} |
|||
} |
|||
|
|||
const shardName = isNonEmptyString(indexNode.getShardName) |
|||
? `${getShardName(indexNode, record)}.csv` |
|||
: "index.csv" |
|||
|
|||
return joinKey(indexDir, shardName) |
|||
} |
|||
|
|||
export const getShardKeysInRange = async ( |
|||
app, |
|||
indexNode, |
|||
indexDir, |
|||
startRecord = null, |
|||
endRecord = null |
|||
) => { |
|||
const startShardName = !startRecord |
|||
? null |
|||
: shardNameFromKey(getIndexedDataKey(indexNode, indexDir, startRecord)) |
|||
|
|||
const endShardName = !endRecord |
|||
? null |
|||
: shardNameFromKey(getIndexedDataKey(indexNode, indexDir, endRecord)) |
|||
|
|||
return $(await getShardMap(app.datastore, indexDir), [ |
|||
filter( |
|||
k => |
|||
(startRecord === null || k >= startShardName) && |
|||
(endRecord === null || k <= endShardName) |
|||
), |
|||
map(k => joinKey(indexDir, `${k}.csv`)), |
|||
]) |
|||
} |
|||
|
|||
export const ensureShardNameIsInShardMap = async ( |
|||
store, |
|||
indexDir, |
|||
indexedDataKey |
|||
) => { |
|||
const map = await getShardMap(store, indexDir) |
|||
const shardName = shardNameFromKey(indexedDataKey) |
|||
if (!includes(shardName)(map)) { |
|||
map.push(shardName) |
|||
await writeShardMap(store, indexDir, map) |
|||
} |
|||
} |
|||
|
|||
export const getShardMap = async (datastore, indexDir) => { |
|||
const shardMapKey = getShardMapKey(indexDir) |
|||
try { |
|||
return await datastore.loadJson(shardMapKey) |
|||
} catch (_) { |
|||
await datastore.createJson(shardMapKey, []) |
|||
return [] |
|||
} |
|||
} |
|||
|
|||
export const writeShardMap = async (datastore, indexDir, shardMap) => |
|||
await datastore.updateJson(getShardMapKey(indexDir), shardMap) |
|||
|
|||
export const getAllShardKeys = async (app, indexNode, indexDir) => |
|||
await getShardKeysInRange(app, indexNode, indexDir) |
|||
|
|||
export const getShardMapKey = indexDir => joinKey(indexDir, "shardMap.json") |
|||
|
|||
export const getUnshardedIndexDataKey = indexDir => |
|||
joinKey(indexDir, "index.csv") |
|||
|
|||
export const createIndexFile = async (datastore, indexedDataKey, index) => { |
|||
if (isShardedIndex(index)) { |
|||
const indexDir = getParentKey(indexedDataKey) |
|||
const shardMap = await getShardMap(datastore, indexDir) |
|||
shardMap.push(shardNameFromKey(indexedDataKey)) |
|||
await writeShardMap(datastore, indexDir, shardMap) |
|||
} |
|||
await datastore.createFile(indexedDataKey, "") |
|||
} |
|||
|
|||
export const shardNameFromKey = key => |
|||
$(key, [splitKey, last]).replace(".csv", "") |
|||
|
|||
export const getIndexKey_BasedOnDecendant = (decendantKey, indexNode) => { |
|||
if (isGlobalIndex(indexNode)) { |
|||
return `${indexNode.nodeKey()}` |
|||
} |
|||
|
|||
const indexedDataParentKey = getActualKeyOfParent( |
|||
indexNode.parent().nodeKey(), |
|||
decendantKey |
|||
) |
|||
|
|||
return joinKey(indexedDataParentKey, indexNode.name) |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
import { find, take, union } from "lodash/fp" |
|||
import { getFlattenedHierarchy } from "../templateApi/hierarchy" |
|||
import { $, splitKey, joinKey } from "../common" |
|||
import { NotFoundError } from "../common/errors" |
|||
|
|||
export const customId = app => (nodeName, id) => { |
|||
const node = $(app.hierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find(n => n.name === nodeName), |
|||
]) |
|||
|
|||
if (!node) throw new NotFoundError(`Cannot find node ${nodeName}`) |
|||
|
|||
return `${node.nodeId}-${id}` |
|||
} |
|||
|
|||
export const setCustomId = app => (record, id) => { |
|||
record.id = customId(app)(record.type, id) |
|||
|
|||
const keyParts = splitKey(record.key) |
|||
|
|||
record.key = $(keyParts, [ |
|||
take(keyParts.length - 1), |
|||
union([record.id]), |
|||
joinKey, |
|||
]) |
|||
|
|||
return record |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
import { safeKey, apiWrapper, events, joinKey } from "../common" |
|||
import { _load } from "./load" |
|||
import { _deleteCollection } from "../collectionApi/delete" |
|||
import { getExactNodeForKey } from "../templateApi/hierarchy" |
|||
import { transactionForDeleteRecord } from "../transactions/create" |
|||
import { permission } from "../authApi/permissions" |
|||
import { getRecordInfo } from "./recordInfo" |
|||
|
|||
export const deleteRecord = (app, disableCleanup = false) => async key => { |
|||
key = safeKey(key) |
|||
return apiWrapper( |
|||
app, |
|||
events.recordApi.delete, |
|||
permission.deleteRecord.isAuthorized(key), |
|||
{ key }, |
|||
_deleteRecord, |
|||
app, |
|||
key, |
|||
disableCleanup |
|||
) |
|||
} |
|||
|
|||
// called deleteRecord because delete is a keyword
|
|||
export const _deleteRecord = async (app, key) => { |
|||
const recordInfo = getRecordInfo(app.hierarchy, key) |
|||
key = recordInfo.key |
|||
const node = getExactNodeForKey(app.hierarchy)(key) |
|||
|
|||
for (const collectionRecord of node.children) { |
|||
const collectionKey = joinKey(key, collectionRecord.collectionName) |
|||
await _deleteCollection(app, collectionKey, true) |
|||
} |
|||
|
|||
await app.datastore.deleteFile(key) |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
import { apiWrapper, events, isNothing } from "../common" |
|||
import { permission } from "../authApi/permissions" |
|||
import { safeGetFullFilePath } from "./uploadFile" |
|||
import { BadRequestError } from "../common/errors" |
|||
import { getRecordInfo } from "./recordInfo" |
|||
|
|||
export const downloadFile = app => async (recordKey, relativePath) => |
|||
apiWrapper( |
|||
app, |
|||
events.recordApi.uploadFile, |
|||
permission.readRecord.isAuthorized(recordKey), |
|||
{ recordKey, relativePath }, //remove dupe key 'recordKey' from object
|
|||
_downloadFile, |
|||
app, |
|||
recordKey, |
|||
relativePath |
|||
) |
|||
|
|||
const _downloadFile = async (app, recordKey, relativePath) => { |
|||
if (isNothing(recordKey)) { |
|||
throw new BadRequestError("Record Key not supplied") |
|||
} |
|||
if (isNothing(relativePath)) { |
|||
throw new BadRequestError("file path not supplied") |
|||
} |
|||
|
|||
const { dir } = getRecordInfo(app.hierarchy, recordKey) |
|||
return await app.datastore.readableFileStream( |
|||
safeGetFullFilePath(dir, relativePath) |
|||
) |
|||
} |
|||
@ -1,76 +0,0 @@ |
|||
import { map, isString, has, some } from "lodash/fp" |
|||
import { |
|||
getExactNodeForKey, |
|||
findField, |
|||
getNode, |
|||
isGlobalIndex, |
|||
} from "../templateApi/hierarchy" |
|||
import { listItems } from "../indexApi/listItems" |
|||
import { $, apiWrapperSync, events, safeKey } from "../common" |
|||
import { getIndexKey_BasedOnDecendant } from "../indexing/sharding" |
|||
import { permission } from "../authApi/permissions" |
|||
|
|||
export const getContext = app => recordKey => { |
|||
recordKey = safeKey(recordKey) |
|||
return apiWrapperSync( |
|||
app, |
|||
events.recordApi.getContext, |
|||
permission.readRecord.isAuthorized(recordKey), |
|||
{ recordKey }, |
|||
_getContext, |
|||
app, |
|||
recordKey |
|||
) |
|||
} |
|||
|
|||
export const _getContext = (app, recordKey) => { |
|||
recordKey = safeKey(recordKey) |
|||
const recordNode = getExactNodeForKey(app.hierarchy)(recordKey) |
|||
|
|||
const cachedReferenceIndexes = {} |
|||
|
|||
const lazyLoadReferenceIndex = async typeOptions => { |
|||
if (!has(typeOptions.indexNodeKey)(cachedReferenceIndexes)) { |
|||
cachedReferenceIndexes[typeOptions.indexNodeKey] = { |
|||
typeOptions, |
|||
data: await readReferenceIndex(app, recordKey, typeOptions), |
|||
} |
|||
} |
|||
|
|||
return cachedReferenceIndexes[typeOptions.indexNodeKey] |
|||
} |
|||
|
|||
const getTypeOptions = typeOptions_or_fieldName => |
|||
isString(typeOptions_or_fieldName) |
|||
? findField(recordNode, typeOptions_or_fieldName).typeOptions |
|||
: typeOptions_or_fieldName |
|||
|
|||
return { |
|||
referenceExists: async (typeOptions_or_fieldName, key) => { |
|||
const typeOptions = getTypeOptions(typeOptions_or_fieldName) |
|||
const { data } = await lazyLoadReferenceIndex(typeOptions) |
|||
return some(i => i.key === key)(data) |
|||
}, |
|||
referenceOptions: async typeOptions_or_fieldName => { |
|||
const typeOptions = getTypeOptions(typeOptions_or_fieldName) |
|||
const { data } = await lazyLoadReferenceIndex(typeOptions) |
|||
return data |
|||
}, |
|||
recordNode, |
|||
} |
|||
} |
|||
|
|||
const readReferenceIndex = async (app, recordKey, typeOptions) => { |
|||
const indexNode = getNode(app.hierarchy, typeOptions.indexNodeKey) |
|||
const indexKey = isGlobalIndex(indexNode) |
|||
? indexNode.nodeKey() |
|||
: getIndexKey_BasedOnDecendant(recordKey, indexNode) |
|||
|
|||
const items = await listItems(app)(indexKey) |
|||
return $(items, [ |
|||
map(i => ({ |
|||
key: i.key, |
|||
value: i[typeOptions.displayValue], |
|||
})), |
|||
]) |
|||
} |
|||
@ -1,51 +0,0 @@ |
|||
import { keyBy, mapValues } from "lodash/fp" |
|||
import { generate } from "shortid" |
|||
import { |
|||
getNodeForCollectionPath, |
|||
isSingleRecord, |
|||
} from "../templateApi/hierarchy" |
|||
import { getNewFieldValue } from "../types" |
|||
import { $, joinKey, safeKey, apiWrapperSync, events } from "../common" |
|||
import { permission } from "../authApi/permissions" |
|||
|
|||
export const getNew = app => (collectionKey, recordTypeName) => { |
|||
const recordNode = getRecordNode(app, collectionKey, recordTypeName) |
|||
collectionKey = safeKey(collectionKey) |
|||
return apiWrapperSync( |
|||
app, |
|||
events.recordApi.getNew, |
|||
permission.createRecord.isAuthorized(recordNode.nodeKey()), |
|||
{ collectionKey, recordTypeName }, |
|||
_getNew, |
|||
recordNode, |
|||
collectionKey |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* Constructs a record object that can be saved to the backend. |
|||
* @param {*} recordNode - record |
|||
* @param {*} collectionKey - nested collection key that the record will be saved to. |
|||
*/ |
|||
export const _getNew = (recordNode, collectionKey) => |
|||
constructRecord(recordNode, getNewFieldValue, collectionKey) |
|||
|
|||
const getRecordNode = (app, collectionKey) => { |
|||
collectionKey = safeKey(collectionKey) |
|||
return getNodeForCollectionPath(app.hierarchy)(collectionKey) |
|||
} |
|||
|
|||
export const getNewChild = app => (recordKey, collectionName, recordTypeName) => |
|||
getNew(app)(joinKey(recordKey, collectionName), recordTypeName) |
|||
|
|||
export const constructRecord = (recordNode, getFieldValue, collectionKey) => { |
|||
const record = $(recordNode.fields, [keyBy("name"), mapValues(getFieldValue)]) |
|||
|
|||
record.id = `${recordNode.nodeId}-${generate()}` |
|||
record.key = isSingleRecord(recordNode) |
|||
? joinKey(collectionKey, recordNode.name) |
|||
: joinKey(collectionKey, record.id) |
|||
record.isNew = true |
|||
record.type = recordNode.name |
|||
return record |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
import { getNew, getNewChild } from "./getNew" |
|||
import { load } from "./load" |
|||
import { validate } from "./validate" |
|||
import { getContext } from "./getContext" |
|||
import { save } from "./save" |
|||
import { deleteRecord } from "./delete" |
|||
import { uploadFile } from "./uploadFile" |
|||
import { downloadFile } from "./downloadFile" |
|||
import { customId, setCustomId } from "./customId" |
|||
|
|||
const api = app => ({ |
|||
getNew: getNew(app), |
|||
getNewChild: getNewChild(app), |
|||
save: save(app), |
|||
load: load(app), |
|||
delete: deleteRecord(app, false), |
|||
validate: validate(app), |
|||
getContext: getContext(app), |
|||
uploadFile: uploadFile(app), |
|||
downloadFile: downloadFile(app), |
|||
customId: customId(app), |
|||
setCustomId: setCustomId(app), |
|||
}) |
|||
|
|||
export const getRecordApi = app => api(app) |
|||
|
|||
export default getRecordApi |
|||
@ -1,79 +0,0 @@ |
|||
import { isString, flatten, map, filter } from "lodash/fp" |
|||
import { initialiseChildCollections } from "../collectionApi/initialise" |
|||
import { _loadFromInfo } from "./load" |
|||
import { $, joinKey } from "../common" |
|||
import { |
|||
getFlattenedHierarchy, |
|||
isModel, |
|||
getNode, |
|||
isTopLevelRecord, |
|||
fieldReversesReferenceToNode, |
|||
} from "../templateApi/hierarchy" |
|||
import { initialiseIndex } from "../indexing/initialiseIndex" |
|||
import { getRecordInfo } from "./recordInfo" |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
|
|||
export const initialiseChildren = async (app, recordInfoOrKey) => { |
|||
const recordInfo = isString(recordInfoOrKey) |
|||
? getRecordInfo(app.hierarchy, recordInfoOrKey) |
|||
: recordInfoOrKey |
|||
await initialiseReverseReferenceIndexes(app, recordInfo) |
|||
await initialiseAncestorIndexes(app, recordInfo) |
|||
await initialiseChildCollections(app, recordInfo) |
|||
} |
|||
|
|||
export const initialiseChildrenForNode = async (app, recordNode) => { |
|||
if (isTopLevelRecord(recordNode)) { |
|||
await initialiseChildren(app, recordNode.nodeKey()) |
|||
return |
|||
} |
|||
|
|||
const iterate = await getAllIdsIterator(app)( |
|||
recordNode.parent().collectionNodeKey() |
|||
) |
|||
let iterateResult = await iterate() |
|||
while (!iterateResult.done) { |
|||
const { result } = iterateResult |
|||
for (const id of result.ids) { |
|||
const initialisingRecordKey = joinKey(result.collectionKey, id) |
|||
await initialiseChildren(app, initialisingRecordKey) |
|||
} |
|||
iterateResult = await iterate() |
|||
} |
|||
} |
|||
|
|||
const initialiseAncestorIndexes = async (app, recordInfo) => { |
|||
for (const index of recordInfo.recordNode.indexes) { |
|||
const indexKey = recordInfo.child(index.name) |
|||
if (!(await app.datastore.exists(indexKey))) { |
|||
await initialiseIndex(app.datastore, recordInfo.dir, index) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const initialiseReverseReferenceIndexes = async (app, recordInfo) => { |
|||
const indexNodes = $( |
|||
fieldsThatReferenceThisModel(app, recordInfo.recordNode), |
|||
[ |
|||
map(f => |
|||
$(f.typeOptions.reverseIndexNodeKeys, [ |
|||
map(n => getNode(app.hierarchy, n)), |
|||
]) |
|||
), |
|||
flatten, |
|||
] |
|||
) |
|||
|
|||
for (const indexNode of indexNodes) { |
|||
await initialiseIndex(app.datastore, recordInfo.dir, indexNode) |
|||
} |
|||
} |
|||
|
|||
const fieldsThatReferenceThisModel = (app, recordNode) => |
|||
$(app.hierarchy, [ |
|||
getFlattenedHierarchy, |
|||
filter(isModel), |
|||
map(n => n.fields), |
|||
flatten, |
|||
filter(fieldReversesReferenceToNode(recordNode)), |
|||
]) |
|||
@ -1,77 +0,0 @@ |
|||
import { keyBy, mapValues, filter, map, includes, last } from "lodash/fp" |
|||
import { getNode, getExactNodeForKey } from "../templateApi/hierarchy" |
|||
import { safeParseField } from "../types" |
|||
import { |
|||
$, |
|||
splitKey, |
|||
safeKey, |
|||
isNonEmptyString, |
|||
apiWrapper, |
|||
events, |
|||
joinKey, |
|||
} from "../common" |
|||
import { mapRecord } from "../indexing/evaluate" |
|||
import { permission } from "../authApi/permissions" |
|||
|
|||
export const _loadFromInfo = () => {}; |
|||
|
|||
export const getRecordFileName = key => joinKey(key, "record.json") |
|||
|
|||
export const load = app => async key => { |
|||
key = safeKey(key) |
|||
return apiWrapper( |
|||
app, |
|||
events.recordApi.load, |
|||
permission.readRecord.isAuthorized(key), |
|||
{ key }, |
|||
_load, |
|||
app, |
|||
key |
|||
) |
|||
} |
|||
|
|||
export const _load = async (app, key, keyStack = []) => { |
|||
const recordNode = getExactNodeForKey(app.hierarchy)(key) |
|||
const storedData = await app.datastore.loadJson(key) |
|||
|
|||
const loadedRecord = $(recordNode.fields, [ |
|||
keyBy("name"), |
|||
mapValues(f => safeParseField(f, storedData)), |
|||
]) |
|||
|
|||
const newKeyStack = [...keyStack, key] |
|||
|
|||
const references = $(recordNode.fields, [ |
|||
filter( |
|||
f => |
|||
f.type === "reference" && |
|||
isNonEmptyString(loadedRecord[f.name].key) && |
|||
!includes(loadedRecord[f.name].key)(newKeyStack) |
|||
), |
|||
map(f => ({ |
|||
promise: _load(app, loadedRecord[f.name].key, newKeyStack), |
|||
index: getNode(app.hierarchy, f.typeOptions.indexNodeKey), |
|||
field: f, |
|||
})), |
|||
]) |
|||
|
|||
if (references.length > 0) { |
|||
const refRecords = await Promise.all(map(p => p.promise)(references)) |
|||
|
|||
for (const ref of references) { |
|||
loadedRecord[ref.field.name] = mapRecord( |
|||
refRecords[references.indexOf(ref)], |
|||
ref.index |
|||
) |
|||
} |
|||
} |
|||
|
|||
loadedRecord._rev = storedData._rev |
|||
loadedRecord._id = storedData._id |
|||
loadedRecord.key = key |
|||
loadedRecord.id = $(key, [splitKey, last]) |
|||
loadedRecord.type = recordNode.name |
|||
return loadedRecord |
|||
} |
|||
|
|||
export default load |
|||
@ -1,112 +0,0 @@ |
|||
import { |
|||
getExactNodeForKey, |
|||
getActualKeyOfParent, |
|||
isRoot, |
|||
isSingleRecord, |
|||
getNodeForCollectionPath, |
|||
} from "../templateApi/hierarchy" |
|||
import { reduce, find, filter } from "lodash/fp" |
|||
import { $, getFileFromKey, joinKey, safeKey, keySep } from "../common" |
|||
import { folderStructureArray, allIdChars } from "../indexing/allIds" |
|||
|
|||
export const getRecordInfo = (hierarchy, key) => { |
|||
const recordNode = getExactNodeForKey(hierarchy)(key) |
|||
const pathInfo = getRecordDirectory(recordNode, key) |
|||
const dir = joinKey(pathInfo.base, ...pathInfo.subdirs) |
|||
|
|||
return { |
|||
recordJson: recordJson(dir), |
|||
files: files(dir), |
|||
child: name => joinKey(dir, name), |
|||
key: safeKey(key), |
|||
recordNode, |
|||
pathInfo, |
|||
dir, |
|||
} |
|||
} |
|||
|
|||
export const getCollectionDir = (hierarchy, collectionKey) => { |
|||
const recordNode = getNodeForCollectionPath(hierarchy)(collectionKey) |
|||
const dummyRecordKey = joinKey(collectionKey, "1-abcd") |
|||
const pathInfo = getRecordDirectory(recordNode, dummyRecordKey) |
|||
return pathInfo.base |
|||
} |
|||
|
|||
const recordJson = dir => joinKey(dir, "record.json") |
|||
|
|||
const files = dir => joinKey(dir, "files") |
|||
|
|||
const getRecordDirectory = (recordNode, key) => { |
|||
const id = getFileFromKey(key) |
|||
|
|||
const traverseParentKeys = (n, parents = []) => { |
|||
if (isRoot(n)) return parents |
|||
const k = getActualKeyOfParent(n.nodeKey(), key) |
|||
const thisNodeDir = { |
|||
node: n, |
|||
relativeDir: joinKey(recordRelativeDirectory(n, getFileFromKey(k))), |
|||
} |
|||
return traverseParentKeys(n.parent(), [thisNodeDir, ...parents]) |
|||
} |
|||
|
|||
const parentDirs = $(recordNode.parent(), [ |
|||
traverseParentKeys, |
|||
reduce((key, item) => { |
|||
return joinKey(key, item.node.collectionName, item.relativeDir) |
|||
}, keySep), |
|||
]) |
|||
|
|||
const subdirs = isSingleRecord(recordNode) |
|||
? [] |
|||
: recordRelativeDirectory(recordNode, id) |
|||
const base = isSingleRecord(recordNode) |
|||
? joinKey(parentDirs, recordNode.name) |
|||
: joinKey(parentDirs, recordNode.collectionName) |
|||
|
|||
return { |
|||
subdirs, |
|||
base, |
|||
} |
|||
} |
|||
|
|||
const recordRelativeDirectory = (recordNode, id) => { |
|||
const folderStructure = folderStructureArray(recordNode) |
|||
const strippedId = id.substring(recordNode.nodeId.toString().length + 1) |
|||
const subfolders = $(folderStructure, [ |
|||
reduce( |
|||
(result, currentCount) => { |
|||
result.folders.push( |
|||
folderForChar(strippedId[result.level], currentCount) |
|||
) |
|||
return { level: result.level + 1, folders: result.folders } |
|||
}, |
|||
{ level: 0, folders: [] } |
|||
), |
|||
f => f.folders, |
|||
filter(f => !!f), |
|||
]) |
|||
|
|||
return [recordNode.nodeId.toString(), ...subfolders, id] |
|||
} |
|||
|
|||
const folderForChar = (char, folderCount) => |
|||
folderCount === 1 |
|||
? "" |
|||
: $(folderCount, [idFoldersForFolderCount, find(f => f.includes(char))]) |
|||
|
|||
const idFoldersForFolderCount = folderCount => { |
|||
const charRangePerShard = 64 / folderCount |
|||
const idFolders = [] |
|||
let index = 0 |
|||
let currentIdsShard = "" |
|||
while (index < 64) { |
|||
currentIdsShard += allIdChars[index] |
|||
if ((index + 1) % charRangePerShard === 0) { |
|||
idFolders.push(currentIdsShard) |
|||
currentIdsShard = "" |
|||
} |
|||
index++ |
|||
} |
|||
|
|||
return idFolders |
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
import { cloneDeep } from "lodash/fp" |
|||
import { validate } from "./validate" |
|||
import { _load } from "./load" |
|||
import { apiWrapper, events } from "../common" |
|||
import { permission } from "../authApi/permissions" |
|||
import { BadRequestError } from "../common/errors" |
|||
import { getExactNodeForKey } from "../templateApi/hierarchy" |
|||
|
|||
export const save = app => async (record, context) => |
|||
apiWrapper( |
|||
app, |
|||
events.recordApi.save, |
|||
record._rev |
|||
? permission.updateRecord.isAuthorized(record.key) |
|||
: permission.createRecord.isAuthorized(record.key), |
|||
{ record }, |
|||
_save, |
|||
app, |
|||
record, |
|||
context, |
|||
false |
|||
) |
|||
|
|||
export const _save = async (app, record, context, skipValidation = false) => { |
|||
const recordClone = cloneDeep(record) |
|||
if (!skipValidation) { |
|||
const validationResult = await validate(app)(recordClone, context) |
|||
if (!validationResult.isValid) { |
|||
await app.publish(events.recordApi.save.onInvalid, { |
|||
record, |
|||
validationResult, |
|||
}) |
|||
throw new BadRequestError( |
|||
`Save : Record Invalid : ${JSON.stringify(validationResult.errors)}` |
|||
) |
|||
} |
|||
} |
|||
|
|||
const recordNode = getExactNodeForKey(app.hierarchy)(record.key) |
|||
|
|||
recordClone.nodeKey = recordNode.nodeKey() |
|||
|
|||
if (!record._rev) { |
|||
if (!recordNode) throw new Error("Cannot find node for " + record.key) |
|||
|
|||
// FILES
|
|||
// await app.datastore.createFolder(files)
|
|||
await app.datastore.createJson(record.key, recordClone) |
|||
await app.publish(events.recordApi.save.onRecordCreated, { |
|||
record: recordClone, |
|||
}) |
|||
} else { |
|||
const oldRecord = await _load(app, record.key) |
|||
await app.datastore.updateJson(record.key, recordClone) |
|||
await app.publish(events.recordApi.save.onRecordUpdated, { |
|||
old: oldRecord, |
|||
new: recordClone, |
|||
}) |
|||
} |
|||
|
|||
// TODO: use nano.head to get _rev (saves loading whole doc)
|
|||
const savedResult = await app.datastore.loadFile(record.key) |
|||
recordClone._rev = savedResult._rev |
|||
return recordClone |
|||
} |
|||
@ -1,146 +0,0 @@ |
|||
import { includes, filter, map, some } from "lodash/fp" |
|||
import { generate } from "shortid" |
|||
import { _loadFromInfo } from "./load" |
|||
import { |
|||
apiWrapper, |
|||
events, |
|||
splitKey, |
|||
$, |
|||
joinKey, |
|||
isNothing, |
|||
tryAwaitOrIgnore, |
|||
} from "../common" |
|||
import { getExactNodeForKey } from "../templateApi/hierarchy" |
|||
import { permission } from "../authApi/permissions" |
|||
import { isLegalFilename } from "../types/file" |
|||
import { BadRequestError, ForbiddenError } from "../common/errors" |
|||
import { getRecordInfo } from "./recordInfo" |
|||
|
|||
export const uploadFile = app => async ( |
|||
recordKey, |
|||
readableStream, |
|||
relativeFilePath |
|||
) => |
|||
apiWrapper( |
|||
app, |
|||
events.recordApi.uploadFile, |
|||
permission.updateRecord.isAuthorized(recordKey), |
|||
{ recordKey, readableStream, relativeFilePath }, |
|||
_uploadFile, |
|||
app, |
|||
recordKey, |
|||
readableStream, |
|||
relativeFilePath |
|||
) |
|||
|
|||
const _uploadFile = async ( |
|||
app, |
|||
recordKey, |
|||
readableStream, |
|||
relativeFilePath |
|||
) => { |
|||
if (isNothing(recordKey)) { |
|||
throw new BadRequestError("Record Key not supplied") |
|||
} |
|||
if (isNothing(relativeFilePath)) { |
|||
throw new BadRequestError("file path not supplied") |
|||
} |
|||
if (!isLegalFilename(relativeFilePath)) { |
|||
throw new BadRequestError("Illegal filename") |
|||
} |
|||
|
|||
const recordInfo = getRecordInfo(app.hierarchy, recordKey) |
|||
const record = await _loadFromInfo(app, recordInfo) |
|||
|
|||
const fullFilePath = safeGetFullFilePath(recordInfo.dir, relativeFilePath) |
|||
|
|||
const tempFilePath = `${fullFilePath}_${generate()}.temp` |
|||
|
|||
const outputStream = await app.datastore.writableFileStream(tempFilePath) |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
readableStream.pipe(outputStream) |
|||
outputStream.on("error", reject) |
|||
outputStream.on("finish", resolve) |
|||
}) |
|||
.then(() => app.datastore.getFileSize(tempFilePath)) |
|||
.then(size => { |
|||
const isExpectedFileSize = checkFileSizeAgainstFields( |
|||
app, |
|||
record, |
|||
relativeFilePath, |
|||
size |
|||
) |
|||
if (!isExpectedFileSize) { |
|||
throw new BadRequestError( |
|||
`Fields for ${relativeFilePath} do not have expected size.` |
|||
) |
|||
} |
|||
}) |
|||
.then(() => tryAwaitOrIgnore(app.datastore.deleteFile, fullFilePath)) |
|||
.then(() => app.datastore.renameFile(tempFilePath, fullFilePath)) |
|||
} |
|||
|
|||
const checkFileSizeAgainstFields = ( |
|||
app, |
|||
record, |
|||
relativeFilePath, |
|||
expectedSize |
|||
) => { |
|||
const recordNode = getExactNodeForKey(app.hierarchy)(record.key) |
|||
|
|||
const incorrectFileFields = $(recordNode.fields, [ |
|||
filter( |
|||
f => |
|||
f.type === "file" && |
|||
record[f.name].relativePath === relativeFilePath && |
|||
record[f.name].size !== expectedSize |
|||
), |
|||
map(f => f.name), |
|||
]) |
|||
|
|||
const incorrectFileArrayFields = $(recordNode.fields, [ |
|||
filter( |
|||
a => |
|||
a.type === "array<file>" && |
|||
$(record[a.name], [ |
|||
some( |
|||
f => |
|||
record[f.name].relativePath === relativeFilePath && |
|||
record[f.name].size !== expectedSize |
|||
), |
|||
]) |
|||
), |
|||
map(f => f.name), |
|||
]) |
|||
|
|||
const incorrectFields = [...incorrectFileFields, ...incorrectFileArrayFields] |
|||
|
|||
if (incorrectFields.length > 0) { |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
export const safeGetFullFilePath = (recordDir, relativeFilePath) => { |
|||
const naughtyUser = () => { |
|||
throw new ForbiddenError("naughty naughty") |
|||
} |
|||
|
|||
if (relativeFilePath.startsWith("..")) naughtyUser() |
|||
|
|||
const pathParts = splitKey(relativeFilePath) |
|||
|
|||
if (includes("..")(pathParts)) naughtyUser() |
|||
|
|||
const recordKeyParts = splitKey(recordDir) |
|||
|
|||
const fullPathParts = [ |
|||
...recordKeyParts, |
|||
"files", |
|||
...filter(p => p !== ".")(pathParts), |
|||
] |
|||
|
|||
return joinKey(fullPathParts) |
|||
} |
|||
@ -1,91 +0,0 @@ |
|||
import { map, reduce, filter, isEmpty, flatten, each } from "lodash/fp" |
|||
import { compileCode } from "../common/compileCode" |
|||
import _ from "lodash" |
|||
import { getExactNodeForKey } from "../templateApi/hierarchy" |
|||
import { validateFieldParse, validateTypeConstraints } from "../types" |
|||
import { $, isNothing, isNonEmptyString } from "../common" |
|||
import { _getContext } from "./getContext" |
|||
|
|||
const fieldParseError = (fieldName, value) => ({ |
|||
fields: [fieldName], |
|||
message: `Could not parse field ${fieldName}:${value}`, |
|||
}) |
|||
|
|||
const validateAllFieldParse = (record, recordNode) => |
|||
$(recordNode.fields, [ |
|||
map(f => ({ name: f.name, parseResult: validateFieldParse(f, record) })), |
|||
reduce((errors, f) => { |
|||
if (f.parseResult.success) return errors |
|||
errors.push(fieldParseError(f.name, f.parseResult.value)) |
|||
return errors |
|||
}, []), |
|||
]) |
|||
|
|||
const validateAllTypeConstraints = async (record, recordNode, context) => { |
|||
const errors = [] |
|||
for (const field of recordNode.fields) { |
|||
$(await validateTypeConstraints(field, record, context), [ |
|||
filter(isNonEmptyString), |
|||
map(m => ({ message: m, fields: [field.name] })), |
|||
each(e => errors.push(e)), |
|||
]) |
|||
} |
|||
return errors |
|||
} |
|||
|
|||
const runRecordValidationRules = (record, recordNode) => { |
|||
const runValidationRule = rule => { |
|||
const isValid = compileCode(rule.expressionWhenValid) |
|||
const expressionContext = { record, _ } |
|||
return isValid(expressionContext) |
|||
? { valid: true } |
|||
: { |
|||
valid: false, |
|||
fields: rule.invalidFields, |
|||
message: rule.messageWhenInvalid, |
|||
} |
|||
} |
|||
|
|||
return $(recordNode.validationRules, [ |
|||
map(runValidationRule), |
|||
flatten, |
|||
filter(r => r.valid === false), |
|||
map(r => ({ fields: r.fields, message: r.message })), |
|||
]) |
|||
} |
|||
|
|||
export const validate = app => async (record, context) => { |
|||
context = isNothing(context) ? _getContext(app, record.key) : context |
|||
|
|||
const recordNode = getExactNodeForKey(app.hierarchy)(record.key) |
|||
const fieldParseFails = validateAllFieldParse(record, recordNode) |
|||
|
|||
// non parsing would cause further issues - exit here
|
|||
if (!isEmpty(fieldParseFails)) { |
|||
return { isValid: false, errors: fieldParseFails } |
|||
} |
|||
|
|||
const recordValidationRuleFails = runRecordValidationRules(record, recordNode) |
|||
const typeContraintFails = await validateAllTypeConstraints( |
|||
record, |
|||
recordNode, |
|||
context |
|||
) |
|||
|
|||
if ( |
|||
isEmpty(fieldParseFails) && |
|||
isEmpty(recordValidationRuleFails) && |
|||
isEmpty(typeContraintFails) |
|||
) { |
|||
return { isValid: true, errors: [] } |
|||
} |
|||
|
|||
return { |
|||
isValid: false, |
|||
errors: _.union( |
|||
fieldParseFails, |
|||
typeContraintFails, |
|||
recordValidationRuleFails |
|||
), |
|||
} |
|||
} |
|||
@ -1,54 +0,0 @@ |
|||
import { |
|||
findRoot, |
|||
getFlattenedHierarchy, |
|||
fieldReversesReferenceToIndex, |
|||
isModel, |
|||
} from "./hierarchy" |
|||
import { $ } from "../common" |
|||
import { map, filter, reduce } from "lodash/fp" |
|||
|
|||
export const canDeleteIndex = indexNode => { |
|||
const flatHierarchy = $(indexNode, [findRoot, getFlattenedHierarchy]) |
|||
|
|||
const reverseIndexes = $(flatHierarchy, [ |
|||
filter(isModel), |
|||
reduce((obj, r) => { |
|||
for (let field of r.fields) { |
|||
if (fieldReversesReferenceToIndex(indexNode)(field)) { |
|||
obj.push({ ...field, record: r }) |
|||
} |
|||
} |
|||
return obj |
|||
}, []), |
|||
map( |
|||
f => |
|||
`field "${f.name}" on record "${f.record.name}" uses this index as a reference` |
|||
), |
|||
]) |
|||
|
|||
const lookupIndexes = $(flatHierarchy, [ |
|||
filter(isModel), |
|||
reduce((obj, r) => { |
|||
for (let field of r.fields) { |
|||
if ( |
|||
field.type === "reference" && |
|||
field.typeOptions.indexNodeKey === indexNode.nodeKey() |
|||
) { |
|||
obj.push({ ...field, record: r }) |
|||
} |
|||
} |
|||
return obj |
|||
}, []), |
|||
map( |
|||
f => |
|||
`field "${f.name}" on record "${f.record.name}" uses this index as a lookup` |
|||
), |
|||
]) |
|||
|
|||
const errors = [...reverseIndexes, ...lookupIndexes] |
|||
|
|||
return { |
|||
canDelete: errors.length === 0, |
|||
errors, |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
import { |
|||
findRoot, |
|||
getFlattenedHierarchy, |
|||
fieldReversesReferenceToIndex, |
|||
isModel, |
|||
isAncestorIndex, |
|||
isAncestor, |
|||
} from "./hierarchy" |
|||
import { $ } from "../common" |
|||
import { map, filter, includes } from "lodash/fp" |
|||
|
|||
export const canDeleteModel = modelNode => { |
|||
const flatHierarchy = $(modelNode, [findRoot, getFlattenedHierarchy]) |
|||
|
|||
const ancestors = $(flatHierarchy, [filter(isAncestor(modelNode))]) |
|||
|
|||
const belongsToAncestor = i => ancestors.includes(i.parent()) |
|||
|
|||
const errorsForNode = node => { |
|||
const errorsThisNode = $(flatHierarchy, [ |
|||
filter( |
|||
i => |
|||
isAncestorIndex(i) && |
|||
belongsToAncestor(i) && |
|||
includes(node.nodeId)(i.allowedModelNodeIds) |
|||
), |
|||
map( |
|||
i => |
|||
`index "${i.name}" indexes this model. Please remove the model from the index, or delete the index` |
|||
), |
|||
]) |
|||
|
|||
for (let child of node.children) { |
|||
for (let err of errorsForNode(child)) { |
|||
errorsThisNode.push(err) |
|||
} |
|||
} |
|||
|
|||
return errorsThisNode |
|||
} |
|||
|
|||
const errors = errorsForNode(modelNode) |
|||
|
|||
return { errors, canDelete: errors.length === 0 } |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
export const createTrigger = () => ({ |
|||
actionName: "", |
|||
eventName: "", |
|||
// function, has access to event context,
|
|||
// returns object that is used as parameter to action
|
|||
// only used if triggered by event
|
|||
optionsCreator: "", |
|||
// action runs if true,
|
|||
// has access to event context
|
|||
condition: "", |
|||
}) |
|||
|
|||
export const createAction = () => ({ |
|||
name: "", |
|||
behaviourSource: "", |
|||
// name of function in actionSource
|
|||
behaviourName: "", |
|||
// parameter passed into behaviour.
|
|||
// any other parms passed at runtime e.g.
|
|||
// by trigger, or manually, will be merged into this
|
|||
initialOptions: {}, |
|||
}) |
|||
@ -1,242 +0,0 @@ |
|||
import { each, find } from "lodash" |
|||
import { map, max, constant } from "lodash/fp" |
|||
import { |
|||
switchCase, |
|||
defaultCase, |
|||
joinKey, |
|||
$, |
|||
isNothing, |
|||
isSomething, |
|||
} from "../common" |
|||
import { |
|||
isIndex, |
|||
isRoot, |
|||
isSingleRecord, |
|||
isCollectionRecord, |
|||
isModel, |
|||
isaggregateGroup, |
|||
getFlattenedHierarchy, |
|||
} from "./hierarchy" |
|||
import { all } from "../types" |
|||
import { BadRequestError } from "../common/errors" |
|||
|
|||
export const createNodeErrors = { |
|||
indexCannotBeParent: "Index template cannot be a parent", |
|||
allNonRootNodesMustHaveParent: "Only the root node may have no parent", |
|||
indexParentMustBeRecordOrRoot: |
|||
"An index may only have a record or root as a parent", |
|||
aggregateParentMustBeAnIndex: "aggregateGroup parent must be an index", |
|||
} |
|||
|
|||
const pathRegxMaker = node => () => |
|||
node.nodeKey().replace(/{id}/g, "[a-zA-Z0-9_-]+") |
|||
|
|||
const nodeKeyMaker = node => () => |
|||
switchCase( |
|||
[ |
|||
n => isModel(n) && !isSingleRecord(n), |
|||
n => |
|||
joinKey( |
|||
node.parent().nodeKey(), |
|||
node.collectionName, |
|||
`${n.nodeId}-{id}` |
|||
), |
|||
], |
|||
|
|||
[isRoot, constant("/")], |
|||
|
|||
[defaultCase, n => joinKey(node.parent().nodeKey(), n.name)] |
|||
)(node) |
|||
|
|||
const nodeNameMaker = node => () => |
|||
isRoot(node) |
|||
? "/" |
|||
: joinKey(node.parent().nodeName(), node.name) |
|||
|
|||
const validate = parent => node => { |
|||
if ( |
|||
isIndex(node) && |
|||
isSomething(parent) && |
|||
!isRoot(parent) && |
|||
!isModel(parent) |
|||
) { |
|||
throw new BadRequestError(createNodeErrors.indexParentMustBeRecordOrRoot) |
|||
} |
|||
|
|||
if (isaggregateGroup(node) && isSomething(parent) && !isIndex(parent)) { |
|||
throw new BadRequestError(createNodeErrors.aggregateParentMustBeAnIndex) |
|||
} |
|||
|
|||
if (isNothing(parent) && !isRoot(node)) { |
|||
throw new BadRequestError(createNodeErrors.allNonRootNodesMustHaveParent) |
|||
} |
|||
|
|||
return node |
|||
} |
|||
|
|||
const construct = parent => node => { |
|||
node.nodeKey = nodeKeyMaker(node) |
|||
node.nodeName = nodeNameMaker(node) |
|||
node.pathRegx = pathRegxMaker(node) |
|||
node.parent = constant(parent) |
|||
node.isRoot = () => |
|||
isNothing(parent) && node.name === "root" && node.type === "root" |
|||
if (isCollectionRecord(node)) { |
|||
node.collectionNodeKey = () => |
|||
joinKey(parent.nodeKey(), node.collectionName) |
|||
node.collectionPathRegx = () => |
|||
joinKey(parent.pathRegx(), node.collectionName) |
|||
} |
|||
return node |
|||
} |
|||
|
|||
const addToParent = obj => { |
|||
const parent = obj.parent() |
|||
if (isSomething(parent)) { |
|||
if (isIndex(obj)) { |
|||
// Q: why are indexes not children ?
|
|||
// A: because they cannot have children of their own.
|
|||
parent.indexes.push(obj) |
|||
} else if (isaggregateGroup(obj)) { |
|||
parent.aggregateGroups.push(obj) |
|||
} else { |
|||
parent.children.push(obj) |
|||
} |
|||
|
|||
if (isModel(obj)) { |
|||
const defaultIndex = find( |
|||
parent.indexes, |
|||
i => i.name === `${parent.name}_index` |
|||
) |
|||
if (defaultIndex) { |
|||
defaultIndex.allowedModelNodeIds.push(obj.nodeId) |
|||
} |
|||
} |
|||
} |
|||
return obj |
|||
} |
|||
|
|||
export const constructNode = (parent, obj) => |
|||
$(obj, [construct(parent), validate(parent), addToParent]) |
|||
|
|||
const getNodeId = parentNode => { |
|||
// this case is handled better elsewhere
|
|||
if (!parentNode) return null |
|||
const findRoot = n => (isRoot(n) ? n : findRoot(n.parent())) |
|||
const root = findRoot(parentNode) |
|||
|
|||
return $(root, [getFlattenedHierarchy, map(n => n.nodeId), max]) + 1 |
|||
} |
|||
|
|||
export const constructHierarchy = (node, parent) => { |
|||
construct(parent)(node) |
|||
if (node.indexes) { |
|||
each(node.indexes, child => constructHierarchy(child, node)) |
|||
} |
|||
if (node.aggregateGroups) { |
|||
each(node.aggregateGroups, child => constructHierarchy(child, node)) |
|||
} |
|||
if (node.children && node.children.length > 0) { |
|||
each(node.children, child => constructHierarchy(child, node)) |
|||
} |
|||
if (node.fields) { |
|||
each(node.fields, f => |
|||
each(f.typeOptions, (val, key) => { |
|||
const def = all[f.type].optionDefinitions[key] |
|||
if (!def) { |
|||
// unknown typeOption
|
|||
delete f.typeOptions[key] |
|||
} else { |
|||
f.typeOptions[key] = def.parse(val) |
|||
} |
|||
}) |
|||
) |
|||
} |
|||
return node |
|||
} |
|||
|
|||
export const getNewRootLevel = () => |
|||
construct()({ |
|||
name: "root", |
|||
type: "root", |
|||
children: [], |
|||
pathMaps: [], |
|||
indexes: [], |
|||
nodeId: 0, |
|||
}) |
|||
|
|||
const _getNewModelTemplate = (parent, name, createDefaultIndex, isSingle) => { |
|||
const nodeId = getNodeId(parent) |
|||
const node = constructNode(parent, { |
|||
name, |
|||
type: "record", |
|||
fields: [], |
|||
children: [], |
|||
validationRules: [], |
|||
nodeId: nodeId, |
|||
indexes: [], |
|||
estimatedRecordCount: isModel(parent) ? 500 : 1000000, |
|||
collectionName: (nodeId || "").toString(), |
|||
isSingle, |
|||
}) |
|||
|
|||
if (createDefaultIndex) { |
|||
const defaultIndex = getNewIndexTemplate(parent) |
|||
defaultIndex.name = `${name}_index` |
|||
defaultIndex.allowedModelNodeIds.push(node.nodeId) |
|||
} |
|||
|
|||
return node |
|||
} |
|||
|
|||
export const getNewModelTemplate = ( |
|||
parent, |
|||
name = "", |
|||
createDefaultIndex = true |
|||
) => _getNewModelTemplate(parent, name, createDefaultIndex, false) |
|||
|
|||
export const getNewSingleRecordTemplate = parent => |
|||
_getNewModelTemplate(parent, "", false, true) |
|||
|
|||
export const getNewIndexTemplate = (parent, type = "ancestor") => |
|||
constructNode(parent, { |
|||
name: "", |
|||
type: "index", |
|||
map: "return {...record};", |
|||
filter: "", |
|||
indexType: type, |
|||
getShardName: "", |
|||
getSortKey: "record.id", |
|||
aggregateGroups: [], |
|||
allowedModelNodeIds: [], |
|||
nodeId: getNodeId(parent), |
|||
}) |
|||
|
|||
export const getNewAggregateGroupTemplate = index => |
|||
constructNode(index, { |
|||
name: "", |
|||
type: "aggregateGroup", |
|||
groupBy: "", |
|||
aggregates: [], |
|||
condition: "", |
|||
nodeId: getNodeId(index), |
|||
}) |
|||
|
|||
export const getNewAggregateTemplate = set => { |
|||
const aggregatedValue = { |
|||
name: "", |
|||
aggregatedValue: "", |
|||
} |
|||
set.aggregates.push(aggregatedValue) |
|||
return aggregatedValue |
|||
} |
|||
|
|||
export default { |
|||
getNewRootLevel, |
|||
getNewModelTemplate, |
|||
getNewIndexTemplate, |
|||
createNodeErrors, |
|||
constructHierarchy, |
|||
getNewAggregateGroupTemplate, |
|||
getNewAggregateTemplate, |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
import { getRecordInfo } from "../recordApi/recordInfo" |
|||
import { isTopLevelIndex, getParentKey, getLastPartInKey } from "./hierarchy" |
|||
import { safeKey, joinKey } from "../common" |
|||
|
|||
export const deleteAllIndexFilesForNode = async (app, indexNode) => { |
|||
if (isTopLevelIndex(indexNode)) { |
|||
await app.datastore.deleteFolder(indexNode.nodeKey()) |
|||
return |
|||
} |
|||
|
|||
const iterate = await getAllIdsIterator(app)(indexNode.parent().nodeKey()) |
|||
let iterateResult = await iterate() |
|||
while (!iterateResult.done) { |
|||
const { result } = iterateResult |
|||
for (const id of result.ids) { |
|||
const deletingIndexKey = joinKey(result.collectionKey, id, indexNode.name) |
|||
await deleteIndexFolder(app, deletingIndexKey) |
|||
} |
|||
iterateResult = await iterate() |
|||
} |
|||
} |
|||
|
|||
const deleteIndexFolder = async (app, indexKey) => { |
|||
indexKey = safeKey(indexKey) |
|||
const indexName = getLastPartInKey(indexKey) |
|||
const parentRecordKey = getParentKey(indexKey) |
|||
const recordInfo = getRecordInfo(app.hierarchy, parentRecordKey) |
|||
await app.datastore.deleteFolder(joinKey(recordInfo.dir, indexName)) |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
import { getCollectionDir } from "../recordApi/recordInfo" |
|||
import { isTopLevelRecord, getCollectionKey } from "./hierarchy" |
|||
import { safeKey, joinKey } from "../common" |
|||
|
|||
export const deleteAllRecordsForNode = async (app, recordNode) => { |
|||
if (isTopLevelRecord(recordNode)) { |
|||
await deleteRecordCollection(app, recordNode.collectionName) |
|||
return |
|||
} |
|||
|
|||
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey()) |
|||
let iterateResult = await iterate() |
|||
while (!iterateResult.done) { |
|||
const { result } = iterateResult |
|||
for (const id of result.ids) { |
|||
const deletingCollectionKey = joinKey( |
|||
result.collectionKey, |
|||
id, |
|||
recordNode.collectionName |
|||
) |
|||
await deleteRecordCollection(app, deletingCollectionKey) |
|||
} |
|||
iterateResult = await iterate() |
|||
} |
|||
} |
|||
|
|||
const deleteRecordCollection = async (app, collectionKey) => { |
|||
collectionKey = safeKey(collectionKey) |
|||
await app.datastore.deleteFolder( |
|||
getCollectionDir(app.hierarchy, collectionKey) |
|||
) |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
import { } from "../templateApi/heirarchy" |
|||
|
|||
export const canDelete = () => { |
|||
/* |
|||
it must not exist on any index.allowedModelNodeIds |
|||
it must not exist on and reference type fields |
|||
these rules should apply to any child nodes , which will also be deleted |
|||
*/ |
|||
} |
|||
@ -1,203 +0,0 @@ |
|||
import { |
|||
getFlattenedHierarchy, |
|||
isModel, |
|||
isIndex, |
|||
isAncestor, |
|||
} from "./hierarchy" |
|||
import { $, none } from "../common" |
|||
import { map, filter, some, find, difference } from "lodash/fp" |
|||
|
|||
export const HierarchyChangeTypes = { |
|||
recordCreated: "Record Created", |
|||
recordDeleted: "Record Deleted", |
|||
recordRenamed: "Record Renamed", |
|||
recordFieldsChanged: "Record Fields Changed", |
|||
recordEstimatedRecordTypeChanged: "Record's Estimated Record Count Changed", |
|||
indexCreated: "Index Created", |
|||
indexDeleted: "Index Deleted", |
|||
indexChanged: "Index Changed", |
|||
} |
|||
|
|||
export const diffHierarchy = (oldHierarchy, newHierarchy) => { |
|||
const oldHierarchyFlat = getFlattenedHierarchy(oldHierarchy) |
|||
const newHierarchyFlat = getFlattenedHierarchy(newHierarchy) |
|||
|
|||
const createdRecords = findCreatedRecords(oldHierarchyFlat, newHierarchyFlat) |
|||
const deletedRecords = findDeletedRecords(oldHierarchyFlat, newHierarchyFlat) |
|||
|
|||
return [ |
|||
...createdRecords, |
|||
...deletedRecords, |
|||
...findRenamedRecords(oldHierarchyFlat, newHierarchyFlat), |
|||
...findRecordsWithFieldsChanged(oldHierarchyFlat, newHierarchyFlat), |
|||
...findRecordsWithEstimatedRecordTypeChanged( |
|||
oldHierarchyFlat, |
|||
newHierarchyFlat |
|||
), |
|||
...findCreatedIndexes(oldHierarchyFlat, newHierarchyFlat, createdRecords), |
|||
...findDeletedIndexes(oldHierarchyFlat, newHierarchyFlat, deletedRecords), |
|||
...findUpdatedIndexes(oldHierarchyFlat, newHierarchyFlat), |
|||
] |
|||
} |
|||
|
|||
const changeItem = (type, oldNode, newNode) => ({ |
|||
type, |
|||
oldNode, |
|||
newNode, |
|||
}) |
|||
|
|||
const findCreatedRecords = (oldHierarchyFlat, newHierarchyFlat) => { |
|||
const allCreated = $(newHierarchyFlat, [ |
|||
filter(isModel), |
|||
filter(nodeDoesNotExistIn(oldHierarchyFlat)), |
|||
map(n => changeItem(HierarchyChangeTypes.recordCreated, null, n)), |
|||
]) |
|||
|
|||
return $(allCreated, [ |
|||
filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(allCreated)), |
|||
]) |
|||
} |
|||
|
|||
const findDeletedRecords = (oldHierarchyFlat, newHierarchyFlat) => { |
|||
const allDeleted = $(oldHierarchyFlat, [ |
|||
filter(isModel), |
|||
filter(nodeDoesNotExistIn(newHierarchyFlat)), |
|||
map(n => changeItem(HierarchyChangeTypes.recordDeleted, n, null)), |
|||
]) |
|||
|
|||
return $(allDeleted, [ |
|||
filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(allDeleted)), |
|||
]) |
|||
} |
|||
|
|||
const findRenamedRecords = (oldHierarchyFlat, newHierarchyFlat) => |
|||
$(oldHierarchyFlat, [ |
|||
filter(isModel), |
|||
filter(nodeExistsIn(newHierarchyFlat)), |
|||
filter( |
|||
nodeChanged( |
|||
newHierarchyFlat, |
|||
(_new, old) => _new.collectionKey !== old.collectionKey |
|||
) |
|||
), |
|||
map(n => |
|||
changeItem( |
|||
HierarchyChangeTypes.recordRenamed, |
|||
n, |
|||
findNodeIn(n, newHierarchyFlat) |
|||
) |
|||
), |
|||
]) |
|||
|
|||
const findRecordsWithFieldsChanged = (oldHierarchyFlat, newHierarchyFlat) => |
|||
$(oldHierarchyFlat, [ |
|||
filter(isModel), |
|||
filter(nodeExistsIn(newHierarchyFlat)), |
|||
filter(hasDifferentFields(newHierarchyFlat)), |
|||
map(n => |
|||
changeItem( |
|||
HierarchyChangeTypes.recordFieldsChanged, |
|||
n, |
|||
findNodeIn(n, newHierarchyFlat) |
|||
) |
|||
), |
|||
]) |
|||
|
|||
const findRecordsWithEstimatedRecordTypeChanged = ( |
|||
oldHierarchyFlat, |
|||
newHierarchyFlat |
|||
) => |
|||
$(oldHierarchyFlat, [ |
|||
filter(isModel), |
|||
filter(nodeExistsIn(newHierarchyFlat)), |
|||
filter( |
|||
nodeChanged( |
|||
newHierarchyFlat, |
|||
(_new, old) => _new.estimatedRecordCount !== old.estimatedRecordCount |
|||
) |
|||
), |
|||
map(n => |
|||
changeItem( |
|||
HierarchyChangeTypes.recordEstimatedRecordTypeChanged, |
|||
n, |
|||
findNodeIn(n, newHierarchyFlat) |
|||
) |
|||
), |
|||
]) |
|||
|
|||
const findCreatedIndexes = ( |
|||
oldHierarchyFlat, |
|||
newHierarchyFlat, |
|||
createdRecords |
|||
) => { |
|||
const allCreated = $(newHierarchyFlat, [ |
|||
filter(isIndex), |
|||
filter(nodeDoesNotExistIn(oldHierarchyFlat)), |
|||
map(n => changeItem(HierarchyChangeTypes.indexCreated, null, n)), |
|||
]) |
|||
|
|||
return $(allCreated, [ |
|||
filter(r => none(r2 => isAncestor(r.newNode)(r2.newNode))(createdRecords)), |
|||
]) |
|||
} |
|||
|
|||
const findDeletedIndexes = ( |
|||
oldHierarchyFlat, |
|||
newHierarchyFlat, |
|||
deletedRecords |
|||
) => { |
|||
const allDeleted = $(oldHierarchyFlat, [ |
|||
filter(isIndex), |
|||
filter(nodeDoesNotExistIn(newHierarchyFlat)), |
|||
map(n => changeItem(HierarchyChangeTypes.indexDeleted, n, null)), |
|||
]) |
|||
|
|||
return $(allDeleted, [ |
|||
filter(r => none(r2 => isAncestor(r.oldNode)(r2.oldNode))(deletedRecords)), |
|||
]) |
|||
} |
|||
|
|||
const findUpdatedIndexes = (oldHierarchyFlat, newHierarchyFlat) => |
|||
$(oldHierarchyFlat, [ |
|||
filter(isIndex), |
|||
filter(nodeExistsIn(newHierarchyFlat)), |
|||
filter(nodeChanged(newHierarchyFlat, indexHasChanged)), |
|||
map(n => |
|||
changeItem( |
|||
HierarchyChangeTypes.indexChanged, |
|||
n, |
|||
findNodeIn(n, newHierarchyFlat) |
|||
) |
|||
), |
|||
]) |
|||
|
|||
const hasDifferentFields = otherFlatHierarchy => record1 => { |
|||
const record2 = findNodeIn(record1, otherFlatHierarchy) |
|||
|
|||
if (record1.fields.length !== record2.fields.length) return true |
|||
|
|||
for (let f1 of record1.fields) { |
|||
if (none(isFieldSame(f1))(record2.fields)) return true |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
const indexHasChanged = (_new, old) => |
|||
_new.map !== old.map || |
|||
_new.filter !== old.filter || |
|||
_new.getShardName !== old.getShardName || |
|||
difference(_new.allowedModelNodeIds)(old.allowedModelNodeIds).length > 0 |
|||
|
|||
const isFieldSame = f1 => f2 => f1.name === f2.name && f1.type === f2.type |
|||
|
|||
const nodeDoesNotExistIn = inThis => node => |
|||
none(n => n.nodeId === node.nodeId)(inThis) |
|||
|
|||
const nodeExistsIn = inThis => node => |
|||
some(n => n.nodeId === node.nodeId)(inThis) |
|||
|
|||
const nodeChanged = (inThis, isChanged) => node => |
|||
some(n => n.nodeId === node.nodeId && isChanged(n, node))(inThis) |
|||
|
|||
const findNodeIn = (node, inThis) => find(n => n.nodeId === node.nodeId)(inThis) |
|||
@ -1,96 +0,0 @@ |
|||
import { some, map, filter, keys, includes, countBy, flatten } from "lodash/fp" |
|||
import { |
|||
isSomething, |
|||
$, |
|||
isNonEmptyString, |
|||
isNothingOrEmpty, |
|||
isNothing, |
|||
} from "../common" |
|||
import { all, getDefaultOptions } from "../types" |
|||
import { applyRuleSet, makerule } from "../common/validationCommon" |
|||
import { BadRequestError } from "../common/errors" |
|||
import { generate } from "shortid" |
|||
|
|||
export const fieldErrors = { |
|||
AddFieldValidationFailed: "Add field validation: ", |
|||
} |
|||
|
|||
export const allowedTypes = () => keys(all) |
|||
|
|||
export const getNewField = type => ({ |
|||
id: generate(), |
|||
name: "", // how field is referenced internally
|
|||
type, |
|||
typeOptions: getDefaultOptions(type), |
|||
label: "", // how field is displayed
|
|||
getInitialValue: "default", // function that gets value when initially created
|
|||
getUndefinedValue: "default", // function that gets value when field undefined on record
|
|||
}) |
|||
|
|||
const fieldRules = allFields => [ |
|||
makerule("name", "field name is not set", f => isNonEmptyString(f.name)), |
|||
makerule("type", "field type is not set", f => isNonEmptyString(f.type)), |
|||
makerule("label", "field label is not set", f => isNonEmptyString(f.label)), |
|||
makerule("getInitialValue", "getInitialValue function is not set", f => |
|||
isNonEmptyString(f.getInitialValue) |
|||
), |
|||
makerule("getUndefinedValue", "getUndefinedValue function is not set", f => |
|||
isNonEmptyString(f.getUndefinedValue) |
|||
), |
|||
makerule( |
|||
"name", |
|||
"field name is duplicated", |
|||
f => isNothingOrEmpty(f.name) || countBy("name")(allFields)[f.name] === 1 |
|||
), |
|||
makerule( |
|||
"type", |
|||
"type is unknown", |
|||
f => isNothingOrEmpty(f.type) || some(t => f.type === t)(allowedTypes()) |
|||
), |
|||
] |
|||
|
|||
const typeOptionsRules = field => { |
|||
const type = all[field.type] |
|||
if (isNothing(type)) return [] |
|||
|
|||
const def = optName => type.optionDefinitions[optName] |
|||
|
|||
return $(field.typeOptions, [ |
|||
keys, |
|||
filter(o => isSomething(def(o)) && isSomething(def(o).isValid)), |
|||
map(o => |
|||
makerule(`typeOptions.${o}`, `${def(o).requirementDescription}`, field => |
|||
def(o).isValid(field.typeOptions[o]) |
|||
) |
|||
), |
|||
]) |
|||
} |
|||
|
|||
export const validateField = allFields => field => { |
|||
const everySingleField = includes(field)(allFields) |
|||
? allFields |
|||
: [...allFields, field] |
|||
return applyRuleSet([ |
|||
...fieldRules(everySingleField), |
|||
...typeOptionsRules(field), |
|||
])(field) |
|||
} |
|||
|
|||
export const validateAllFields = recordNode => |
|||
$(recordNode.fields, [map(validateField(recordNode.fields)), flatten]) |
|||
|
|||
export const addField = (recordTemplate, field) => { |
|||
if (isNothingOrEmpty(field.label)) { |
|||
field.label = field.name |
|||
} |
|||
const validationMessages = validateField([...recordTemplate.fields, field])( |
|||
field |
|||
) |
|||
if (validationMessages.length > 0) { |
|||
const errors = map(m => m.error)(validationMessages) |
|||
throw new BadRequestError( |
|||
`${fieldErrors.AddFieldValidationFailed} ${errors.join(", ")}` |
|||
) |
|||
} |
|||
recordTemplate.fields.push(field) |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
import { appDefinitionFile } from "../common" |
|||
import { constructHierarchy } from "./createNodes" |
|||
|
|||
export const getApplicationDefinition = datastore => async () => { |
|||
const exists = await datastore.exists(appDefinitionFile) |
|||
|
|||
if (!exists) throw new Error("Application definition does not exist") |
|||
|
|||
const appDefinition = await datastore.loadJson(appDefinitionFile) |
|||
appDefinition.hierarchy = constructHierarchy(appDefinition.hierarchy) |
|||
return appDefinition |
|||
} |
|||
@ -1,3 +0,0 @@ |
|||
export const getBehaviourSources = async datastore => { |
|||
await datastore.loadFile("/.config/behaviourSources.js") |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
import { includes } from "lodash/fp" |
|||
|
|||
export const getCouchDbView = (hierarchy, indexNode) => { |
|||
const filter = codeAsFunction("filter", indexNode.filter) |
|||
const map = codeAsFunction("map", indexNode.map) |
|||
const allowedIdsFilter |
|||
|
|||
const includeDocs = !map |
|||
|
|||
const couchDbMap = `` |
|||
|
|||
} |
|||
|
|||
const codeAsFunction = (name, code) => { |
|||
if ((code || "").trim().length === 0) return |
|||
|
|||
let safeCode |
|||
|
|||
if (includes("return ")(code)) { |
|||
safeCode = code |
|||
} else { |
|||
let trimmed = code.trim() |
|||
trimmed = trimmed.endsWith(";") |
|||
? trimmed.substring(0, trimmed.length - 1) |
|||
: trimmed |
|||
safeCode = `return (${trimmed})` |
|||
} |
|||
|
|||
return `function ${name}() {
|
|||
${safeCode} |
|||
}` |
|||
} |
|||
@ -1,290 +0,0 @@ |
|||
import { |
|||
find, |
|||
constant, |
|||
map, |
|||
last, |
|||
first, |
|||
split, |
|||
intersection, |
|||
take, |
|||
union, |
|||
includes, |
|||
filter, |
|||
some, |
|||
} from "lodash/fp" |
|||
import { |
|||
$, |
|||
switchCase, |
|||
isNothing, |
|||
isSomething, |
|||
defaultCase, |
|||
splitKey, |
|||
isNonEmptyString, |
|||
joinKey, |
|||
getHashCode, |
|||
} from "../common" |
|||
import { indexTypes } from "./indexes" |
|||
|
|||
export const getFlattenedHierarchy = (appHierarchy, useCached = true) => { |
|||
if (isSomething(appHierarchy.getFlattenedHierarchy) && useCached) { |
|||
return appHierarchy.getFlattenedHierarchy() |
|||
} |
|||
|
|||
const flattenHierarchy = (currentNode, flattened) => { |
|||
flattened.push(currentNode) |
|||
if ( |
|||
(!currentNode.children || currentNode.children.length === 0) && |
|||
(!currentNode.indexes || currentNode.indexes.length === 0) && |
|||
(!currentNode.aggregateGroups || currentNode.aggregateGroups.length === 0) |
|||
) { |
|||
return flattened |
|||
} |
|||
|
|||
const unionIfAny = l2 => l1 => union(l1)(!l2 ? [] : l2) |
|||
|
|||
const children = $( |
|||
[], |
|||
[ |
|||
unionIfAny(currentNode.children), |
|||
unionIfAny(currentNode.indexes), |
|||
unionIfAny(currentNode.aggregateGroups), |
|||
] |
|||
) |
|||
|
|||
for (const child of children) { |
|||
flattenHierarchy(child, flattened) |
|||
} |
|||
return flattened |
|||
} |
|||
|
|||
appHierarchy.getFlattenedHierarchy = () => flattenHierarchy(appHierarchy, []) |
|||
return appHierarchy.getFlattenedHierarchy() |
|||
} |
|||
|
|||
export const getLastPartInKey = key => last(splitKey(key)) |
|||
|
|||
export const getNodesInPath = appHierarchy => key => |
|||
$(appHierarchy, [ |
|||
getFlattenedHierarchy, |
|||
filter(n => new RegExp(`${n.pathRegx()}`).test(key)), |
|||
]) |
|||
|
|||
export const getExactNodeForKey = appHierarchy => key => |
|||
$(appHierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find(n => new RegExp(`${n.pathRegx()}$`).test(key)), |
|||
]) |
|||
|
|||
export const getNodeForCollectionPath = appHierarchy => collectionKey => |
|||
$(appHierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find( |
|||
n => |
|||
isCollectionRecord(n) && |
|||
new RegExp(`${n.collectionPathRegx()}$`).test(collectionKey) |
|||
), |
|||
]) |
|||
|
|||
export const hasMatchingAncestor = ancestorPredicate => decendantNode => |
|||
switchCase( |
|||
[node => isNothing(node.parent()), constant(false)], |
|||
|
|||
[node => ancestorPredicate(node.parent()), constant(true)], |
|||
|
|||
[defaultCase, node => hasMatchingAncestor(ancestorPredicate)(node.parent())] |
|||
)(decendantNode) |
|||
|
|||
export const getNode = (appHierarchy, nodeKey) => |
|||
$(appHierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find( |
|||
n => |
|||
n.nodeKey() === nodeKey || |
|||
(isCollectionRecord(n) && n.collectionNodeKey() === nodeKey) |
|||
), |
|||
]) |
|||
|
|||
export const getCollectionNode = (appHierarchy, nodeKey) => |
|||
$(appHierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find(n => isCollectionRecord(n) && n.collectionNodeKey() === nodeKey), |
|||
]) |
|||
|
|||
export const getNodeByKeyOrNodeKey = (appHierarchy, keyOrNodeKey) => { |
|||
const nodeByKey = getExactNodeForKey(appHierarchy)(keyOrNodeKey) |
|||
return isNothing(nodeByKey) ? getNode(appHierarchy, keyOrNodeKey) : nodeByKey |
|||
} |
|||
|
|||
export const getCollectionNodeByKeyOrNodeKey = (appHierarchy, keyOrNodeKey) => { |
|||
const nodeByKey = getNodeForCollectionPath(appHierarchy)(keyOrNodeKey) |
|||
return isNothing(nodeByKey) |
|||
? getCollectionNode(appHierarchy, keyOrNodeKey) |
|||
: nodeByKey |
|||
} |
|||
|
|||
export const isNode = (appHierarchy, key) => |
|||
isSomething(getExactNodeForKey(appHierarchy)(key)) |
|||
|
|||
export const getActualKeyOfParent = (parentNodeKey, actualChildKey) => |
|||
$(actualChildKey, [ |
|||
splitKey, |
|||
take(splitKey(parentNodeKey).length), |
|||
ks => joinKey(...ks), |
|||
]) |
|||
|
|||
export const getParentKey = key => { |
|||
return $(key, [splitKey, take(splitKey(key).length - 1), joinKey]) |
|||
} |
|||
|
|||
export const isKeyAncestorOf = ancestorKey => decendantNode => |
|||
hasMatchingAncestor(p => p.nodeKey() === ancestorKey)(decendantNode) |
|||
|
|||
export const hasNoMatchingAncestors = parentPredicate => node => |
|||
!hasMatchingAncestor(parentPredicate)(node) |
|||
|
|||
export const findField = (recordNode, fieldName) => |
|||
find(f => f.name == fieldName)(recordNode.fields) |
|||
|
|||
export const isAncestor = decendant => ancestor => |
|||
isKeyAncestorOf(ancestor.nodeKey())(decendant) |
|||
|
|||
export const isDecendant = ancestor => decendant => |
|||
isAncestor(decendant)(ancestor) |
|||
|
|||
export const getRecordNodeId = recordKey => |
|||
$(recordKey, [splitKey, last, getRecordNodeIdFromId]) |
|||
|
|||
export const getRecordNodeIdFromId = recordId => |
|||
$(recordId, [split("-"), first, parseInt]) |
|||
|
|||
export const getRecordNodeById = (hierarchy, recordId) => |
|||
$(hierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find(n => isModel(n) && n.nodeId === getRecordNodeIdFromId(recordId)), |
|||
]) |
|||
|
|||
export const recordNodeIdIsAllowed = indexNode => nodeId => |
|||
indexNode.allowedModelNodeIds.length === 0 || |
|||
includes(nodeId)(indexNode.allowedModelNodeIds) |
|||
|
|||
export const recordNodeIsAllowed = indexNode => recordNode => |
|||
recordNodeIdIsAllowed(indexNode)(recordNode.nodeId) |
|||
|
|||
export const getAllowedRecordNodesForIndex = (appHierarchy, indexNode) => { |
|||
const recordNodes = $(appHierarchy, [getFlattenedHierarchy, filter(isModel)]) |
|||
|
|||
if (isGlobalIndex(indexNode)) { |
|||
return $(recordNodes, [filter(recordNodeIsAllowed(indexNode))]) |
|||
} |
|||
|
|||
if (isAncestorIndex(indexNode)) { |
|||
return $(recordNodes, [ |
|||
filter(isDecendant(indexNode.parent())), |
|||
filter(recordNodeIsAllowed(indexNode)), |
|||
]) |
|||
} |
|||
|
|||
if (isReferenceIndex(indexNode)) { |
|||
return $(recordNodes, [ |
|||
filter(n => some(fieldReversesReferenceToIndex(indexNode))(n.fields)), |
|||
]) |
|||
} |
|||
} |
|||
|
|||
export const getDependantIndexes = (hierarchy, recordNode) => { |
|||
const allIndexes = $(hierarchy, [getFlattenedHierarchy, filter(isIndex)]) |
|||
|
|||
const allowedAncestors = $(allIndexes, [ |
|||
filter(isAncestorIndex), |
|||
filter(i => recordNodeIsAllowed(i)(recordNode)), |
|||
]) |
|||
|
|||
const allowedReference = $(allIndexes, [ |
|||
filter(isReferenceIndex), |
|||
filter(i => some(fieldReversesReferenceToIndex(i))(recordNode.fields)), |
|||
]) |
|||
|
|||
return [...allowedAncestors, ...allowedReference] |
|||
} |
|||
|
|||
export const getNodeFromNodeKeyHash = hierarchy => hash => |
|||
$(hierarchy, [ |
|||
getFlattenedHierarchy, |
|||
find(n => getHashCode(n.nodeKey()) === hash), |
|||
]) |
|||
|
|||
export const isModel = node => isSomething(node) && node.type === "record" |
|||
export const isSingleRecord = node => isModel(node) && node.isSingle |
|||
export const isCollectionRecord = node => isModel(node) && !node.isSingle |
|||
export const isIndex = node => isSomething(node) && node.type === "index" |
|||
export const isaggregateGroup = node => |
|||
isSomething(node) && node.type === "aggregateGroup" |
|||
export const isShardedIndex = node => |
|||
isIndex(node) && isNonEmptyString(node.getShardName) |
|||
export const isRoot = node => isSomething(node) && node.isRoot() |
|||
export const findRoot = node => (isRoot(node) ? node : findRoot(node.parent())) |
|||
export const isDecendantOfARecord = hasMatchingAncestor(isModel) |
|||
export const isGlobalIndex = node => isIndex(node) && isRoot(node.parent()) |
|||
export const isReferenceIndex = node => |
|||
isIndex(node) && node.indexType === indexTypes.reference |
|||
export const isAncestorIndex = node => |
|||
isIndex(node) && node.indexType === indexTypes.ancestor |
|||
export const isTopLevelRecord = node => isRoot(node.parent()) && isModel(node) |
|||
export const isTopLevelIndex = node => isRoot(node.parent()) && isIndex(node) |
|||
export const getCollectionKey = recordKey => |
|||
$(recordKey, [splitKey, parts => joinKey(parts.slice(0, parts.length - 1))]) |
|||
export const fieldReversesReferenceToNode = node => field => |
|||
field.type === "reference" && |
|||
intersection(field.typeOptions.reverseIndexNodeKeys)( |
|||
map(i => i.nodeKey())(node.indexes) |
|||
).length > 0 |
|||
|
|||
export const fieldReversesReferenceToIndex = indexNode => field => |
|||
field.type === "reference" && |
|||
intersection(field.typeOptions.reverseIndexNodeKeys)([indexNode.nodeKey()]) |
|||
.length > 0 |
|||
|
|||
export const nodeNameFromNodeKey = (hierarchy, nodeKey) => { |
|||
const node = getNode(hierarchy, nodeKey) |
|||
return node ? node.nodeName() : "" |
|||
} |
|||
|
|||
export default { |
|||
getLastPartInKey, |
|||
getNodesInPath, |
|||
getExactNodeForKey, |
|||
hasMatchingAncestor, |
|||
getNode, |
|||
getNodeByKeyOrNodeKey, |
|||
isNode, |
|||
getActualKeyOfParent, |
|||
getParentKey, |
|||
isKeyAncestorOf, |
|||
hasNoMatchingAncestors, |
|||
findField, |
|||
isAncestor, |
|||
isDecendant, |
|||
getRecordNodeId, |
|||
getRecordNodeIdFromId, |
|||
getRecordNodeById, |
|||
recordNodeIdIsAllowed, |
|||
recordNodeIsAllowed, |
|||
getAllowedRecordNodesForIndex, |
|||
getNodeFromNodeKeyHash, |
|||
isModel, |
|||
isCollectionRecord, |
|||
isIndex, |
|||
isaggregateGroup, |
|||
isShardedIndex, |
|||
isRoot, |
|||
isDecendantOfARecord, |
|||
isGlobalIndex, |
|||
isReferenceIndex, |
|||
isAncestorIndex, |
|||
fieldReversesReferenceToNode, |
|||
fieldReversesReferenceToIndex, |
|||
getFlattenedHierarchy, |
|||
isTopLevelIndex, |
|||
isTopLevelRecord, |
|||
nodeNameFromNodeKey, |
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
import { |
|||
getNewRootLevel, |
|||
getNewModelTemplate, |
|||
getNewIndexTemplate, |
|||
createNodeErrors, |
|||
constructHierarchy, |
|||
getNewAggregateGroupTemplate, |
|||
getNewSingleRecordTemplate, |
|||
getNewAggregateTemplate, |
|||
constructNode, |
|||
} from "./createNodes" |
|||
import { getNewField, validateField, addField, fieldErrors } from "./fields" |
|||
import { |
|||
getNewRecordValidationRule, |
|||
commonRecordValidationRules, |
|||
addRecordValidationRule, |
|||
} from "./recordValidationRules" |
|||
import { createAction, createTrigger } from "./createActions" |
|||
import { |
|||
validateTriggers, |
|||
validateTrigger, |
|||
validateNode, |
|||
validateActions, |
|||
validateAll, |
|||
} from "./validate" |
|||
import { getApplicationDefinition } from "./getApplicationDefinition" |
|||
import { saveApplicationHierarchy } from "./saveApplicationHierarchy" |
|||
import { saveActionsAndTriggers } from "./saveActionsAndTriggers" |
|||
import { all } from "../types" |
|||
import { getBehaviourSources } from "./getBehaviourSources" |
|||
import { upgradeData } from "./upgradeData" |
|||
|
|||
const api = app => ({ |
|||
getApplicationDefinition: getApplicationDefinition(app.datastore), |
|||
saveApplicationHierarchy: saveApplicationHierarchy(app), |
|||
saveActionsAndTriggers: saveActionsAndTriggers(app), |
|||
getBehaviourSources: () => getBehaviourSources(app.datastore), |
|||
getNewRootLevel, |
|||
constructNode, |
|||
getNewIndexTemplate, |
|||
getNewModelTemplate, |
|||
getNewField, |
|||
validateField, |
|||
addField, |
|||
fieldErrors, |
|||
getNewRecordValidationRule, |
|||
commonRecordValidationRules, |
|||
addRecordValidationRule, |
|||
createAction, |
|||
createTrigger, |
|||
validateActions, |
|||
validateTrigger, |
|||
getNewAggregateGroupTemplate, |
|||
getNewAggregateTemplate, |
|||
constructHierarchy, |
|||
getNewSingleRecordTemplate, |
|||
allTypes: all, |
|||
validateNode, |
|||
validateAll, |
|||
validateTriggers, |
|||
upgradeData: upgradeData(app), |
|||
}) |
|||
|
|||
export const getTemplateApi = app => api(app) |
|||
|
|||
export const errors = createNodeErrors |
|||
|
|||
export default getTemplateApi |
|||
@ -1,55 +0,0 @@ |
|||
import { map, isEmpty, countBy, flatten, includes, join, keys } from "lodash/fp" |
|||
import { } from "lodash" |
|||
import { applyRuleSet, makerule } from "../common/validationCommon" |
|||
import { compileFilter, compileMap } from "../indexing/evaluate" |
|||
import { isNonEmptyString, executesWithoutException, $ } from "../common" |
|||
import { isModel } from "./hierarchy" |
|||
|
|||
export const indexTypes = { reference: "reference", ancestor: "ancestor" } |
|||
|
|||
export const indexRuleSet = [ |
|||
makerule("map", "index has no map function", index => |
|||
isNonEmptyString(index.map) |
|||
), |
|||
makerule( |
|||
"map", |
|||
"index's map function does not compile", |
|||
index => |
|||
!isNonEmptyString(index.map) || |
|||
executesWithoutException(() => compileMap(index)) |
|||
), |
|||
makerule( |
|||
"filter", |
|||
"index's filter function does not compile", |
|||
index => |
|||
!isNonEmptyString(index.filter) || |
|||
executesWithoutException(() => compileFilter(index)) |
|||
), |
|||
makerule("name", "must declare a name for index", index => |
|||
isNonEmptyString(index.name) |
|||
), |
|||
makerule( |
|||
"name", |
|||
"there is a duplicate named index on this node", |
|||
index => |
|||
isEmpty(index.name) || |
|||
countBy("name")(index.parent().indexes)[index.name] === 1 |
|||
), |
|||
makerule( |
|||
"indexType", |
|||
"reference index may only exist on a record node", |
|||
index => |
|||
isModel(index.parent()) || index.indexType !== indexTypes.reference |
|||
), |
|||
makerule( |
|||
"indexType", |
|||
`index type must be one of: ${join(", ")(keys(indexTypes))}`, |
|||
index => includes(index.indexType)(keys(indexTypes)) |
|||
), |
|||
] |
|||
|
|||
export const validateIndex = (index, allReferenceIndexesOnNode) => |
|||
applyRuleSet(indexRuleSet(allReferenceIndexesOnNode))(index) |
|||
|
|||
export const validateAllIndexes = node => |
|||
$(node.indexes, [map(i => validateIndex(i, node.indexes)), flatten]) |
|||
@ -1,27 +0,0 @@ |
|||
import { getAllIdsIterator } from "../indexing/allIds" |
|||
import { getRecordInfo } from "../recordApi/recordInfo" |
|||
import { isTopLevelIndex } from "./hierarchy" |
|||
import { joinKey } from "../common" |
|||
import { initialiseIndex } from "../indexing/initialiseIndex" |
|||
|
|||
export const initialiseNewIndex = async (app, indexNode) => { |
|||
if (isTopLevelIndex(indexNode)) { |
|||
await initialiseIndex(app.datastore, "/", indexNode) |
|||
return |
|||
} |
|||
|
|||
const iterate = await getAllIdsIterator(app)(indexNode.parent().nodeKey()) |
|||
let iterateResult = await iterate() |
|||
while (!iterateResult.done) { |
|||
const { result } = iterateResult |
|||
for (const id of result.ids) { |
|||
const recordKey = joinKey(result.collectionKey, id) |
|||
await initialiseIndex( |
|||
app.datastore, |
|||
getRecordInfo(app.hierarchy, recordKey).dir, |
|||
indexNode |
|||
) |
|||
} |
|||
iterateResult = await iterate() |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
import { isNumber, isBoolean, defaultCase } from "lodash/fp" |
|||
import { switchCase } from "../common" |
|||
|
|||
export const getNewRecordValidationRule = ( |
|||
invalidFields, |
|||
messageWhenInvalid, |
|||
expressionWhenValid |
|||
) => ({ |
|||
invalidFields, |
|||
messageWhenInvalid, |
|||
expressionWhenValid, |
|||
}) |
|||
|
|||
const getStaticValue = switchCase( |
|||
[isNumber, v => v.toString()], |
|||
[isBoolean, v => v.toString()], |
|||
[defaultCase, v => `'${v}'`] |
|||
) |
|||
|
|||
export const commonRecordValidationRules = { |
|||
fieldNotEmpty: fieldName => |
|||
getNewRecordValidationRule( |
|||
[fieldName], |
|||
`${fieldName} is empty`, |
|||
`!_.isEmpty(record['${fieldName}'])` |
|||
), |
|||
|
|||
fieldBetween: (fieldName, min, max) => |
|||
getNewRecordValidationRule( |
|||
[fieldName], |
|||
`${fieldName} must be between ${min.toString()} and ${max.toString()}`, |
|||
`record['${fieldName}'] >= ${getStaticValue( |
|||
min |
|||
)} && record['${fieldName}'] <= ${getStaticValue(max)} ` |
|||
), |
|||
|
|||
fieldGreaterThan: (fieldName, min, max) => |
|||
getNewRecordValidationRule( |
|||
[fieldName], |
|||
`${fieldName} must be greater than ${min.toString()} and ${max.toString()}`, |
|||
`record['${fieldName}'] >= ${getStaticValue(min)} ` |
|||
), |
|||
} |
|||
|
|||
export const addRecordValidationRule = recordNode => rule => |
|||
recordNode.validationRules.push(rule) |
|||
@ -1,52 +0,0 @@ |
|||
import { join } from "lodash" |
|||
import { map } from "lodash/fp" |
|||
import { appDefinitionFile } from "../common" |
|||
import { validateTriggers, validateActions } from "./validate" |
|||
import { apiWrapper } from "../common/apiWrapper" |
|||
import { events } from "../common/events" |
|||
import { permission } from "../authApi/permissions" |
|||
import { BadRequestError } from "../common/errors" |
|||
|
|||
export const saveActionsAndTriggers = app => async (actions, triggers) => |
|||
apiWrapper( |
|||
app, |
|||
events.templateApi.saveActionsAndTriggers, |
|||
permission.writeTemplates.isAuthorized, |
|||
{ actions, triggers }, |
|||
_saveActionsAndTriggers, |
|||
app.datastore, |
|||
actions, |
|||
triggers |
|||
) |
|||
|
|||
export const _saveActionsAndTriggers = async (datastore, actions, triggers) => { |
|||
if (await datastore.exists(appDefinitionFile)) { |
|||
const appDefinition = await datastore.loadJson(appDefinitionFile) |
|||
appDefinition.actions = actions |
|||
appDefinition.triggers = triggers |
|||
|
|||
const actionValidErrs = map(e => e.error)(validateActions(actions)) |
|||
|
|||
if (actionValidErrs.length > 0) { |
|||
throw new BadRequestError( |
|||
`Actions are invalid: ${join(actionValidErrs, ", ")}` |
|||
) |
|||
} |
|||
|
|||
const triggerValidErrs = map(e => e.error)( |
|||
validateTriggers(triggers, actions) |
|||
) |
|||
|
|||
if (triggerValidErrs.length > 0) { |
|||
throw new BadRequestError( |
|||
`Triggers are invalid: ${join(triggerValidErrs, ", ")}` |
|||
) |
|||
} |
|||
|
|||
await datastore.updateJson(appDefinitionFile, appDefinition) |
|||
} else { |
|||
throw new BadRequestError( |
|||
"Cannot save actions: Application definition does not exist" |
|||
) |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
import { join } from "lodash" |
|||
import { permission } from "../authApi/permissions" |
|||
import { appDefinitionFile } from "../common" |
|||
import { validateAll } from "./validate" |
|||
import { apiWrapper } from "../common/apiWrapper" |
|||
import { events } from "../common/events" |
|||
|
|||
export const saveApplicationHierarchy = app => async hierarchy => |
|||
apiWrapper( |
|||
app, |
|||
events.templateApi.saveApplicationHierarchy, |
|||
permission.writeTemplates.isAuthorized, |
|||
{ hierarchy }, |
|||
_saveApplicationHierarchy, |
|||
app.datastore, |
|||
hierarchy |
|||
) |
|||
|
|||
export const _saveApplicationHierarchy = async (datastore, hierarchy) => { |
|||
const validationErrors = await validateAll(hierarchy) |
|||
if (validationErrors.length > 0) { |
|||
throw new Error( |
|||
`Hierarchy is invalid: ${join( |
|||
validationErrors.map( |
|||
e => `${e.item.nodeKey ? e.item.nodeKey() : ""} : ${e.error}` |
|||
), |
|||
"," |
|||
)}` |
|||
) |
|||
} |
|||
|
|||
if (hierarchy.getFlattenedHierarchy) { |
|||
delete hierarchy.getFlattenedHierarchy |
|||
} |
|||
|
|||
if (await datastore.exists(appDefinitionFile)) { |
|||
const appDefinition = await datastore.loadJson(appDefinitionFile) |
|||
appDefinition.hierarchy = hierarchy |
|||
await datastore.updateJson(appDefinitionFile, appDefinition) |
|||
} else { |
|||
await datastore.createFolder("/.config") |
|||
const appDefinition = { actions: [], triggers: [], hierarchy } |
|||
await datastore.createJson(appDefinitionFile, appDefinition) |
|||
} |
|||
} |
|||
@ -1,208 +0,0 @@ |
|||
import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy" |
|||
import { $, switchCase } from "../common" |
|||
import { |
|||
differenceBy, |
|||
isEqual, |
|||
some, |
|||
map, |
|||
filter, |
|||
uniqBy, |
|||
flatten, |
|||
} from "lodash/fp" |
|||
import { |
|||
findRoot, |
|||
getDependantIndexes, |
|||
isTopLevelRecord, |
|||
isAncestorIndex, |
|||
} from "./hierarchy" |
|||
import { generateSchema } from "../indexing/indexSchemaCreator" |
|||
import { _buildIndex } from "../indexApi/buildIndex" |
|||
import { constructHierarchy } from "./createNodes" |
|||
import { deleteAllRecordsForNode } from "./deleteAllRecordsForNode" |
|||
import { deleteAllIndexFilesForNode } from "./deleteAllIndexFilesForNode" |
|||
import { cloneApp } from "../appInitialise/cloneApp" |
|||
import { initialiseData } from "../appInitialise/initialiseData" |
|||
import { initialiseChildrenForNode } from "../recordApi/initialiseChildren" |
|||
import { initialiseNewIndex } from "./initialiseNewIndex" |
|||
import { _saveApplicationHierarchy } from "../templateApi/saveApplicationHierarchy" |
|||
import { getApplicationDefinition } from "../templateApi/getApplicationDefinition" |
|||
|
|||
export const upgradeData = app => async newHierarchy => { |
|||
const currentAppDef = await getApplicationDefinition(app.datastore)() |
|||
app.hierarchy = currentAppDef.hierarchy |
|||
newHierarchy = constructHierarchy(newHierarchy) |
|||
const diff = diffHierarchy(app.hierarchy, newHierarchy) |
|||
const changeActions = gatherChangeActions(diff) |
|||
|
|||
if (changeActions.length === 0) return |
|||
|
|||
const newApp = |
|||
newHierarchy && |
|||
cloneApp(app, { |
|||
hierarchy: newHierarchy, |
|||
}) |
|||
await doUpgrade(app, newApp, changeActions) |
|||
await _saveApplicationHierarchy(newApp.datastore, newHierarchy) |
|||
} |
|||
|
|||
const gatherChangeActions = diff => |
|||
$(diff, [map(actionForChange), flatten, uniqBy(a => a.compareKey)]) |
|||
|
|||
const doUpgrade = async (oldApp, newApp, changeActions) => { |
|||
for (let action of changeActions) { |
|||
await action.run(oldApp, newApp, action.diff) |
|||
} |
|||
} |
|||
|
|||
const actionForChange = diff => |
|||
switchCase( |
|||
[isChangeType(HierarchyChangeTypes.recordCreated), recordCreatedAction], |
|||
|
|||
[isChangeType(HierarchyChangeTypes.recordDeleted), deleteRecordsAction], |
|||
|
|||
[ |
|||
isChangeType(HierarchyChangeTypes.recordFieldsChanged), |
|||
rebuildAffectedIndexesAction, |
|||
], |
|||
|
|||
[isChangeType(HierarchyChangeTypes.recordRenamed), renameRecordAction], |
|||
|
|||
[ |
|||
isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged), |
|||
reshardRecordsAction, |
|||
], |
|||
|
|||
[isChangeType(HierarchyChangeTypes.indexCreated), newIndexAction], |
|||
|
|||
[isChangeType(HierarchyChangeTypes.indexDeleted), deleteIndexAction], |
|||
|
|||
[isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction] |
|||
)(diff) |
|||
|
|||
const isChangeType = changeType => change => change.type === changeType |
|||
|
|||
const action = (diff, compareKey, run) => ({ |
|||
diff, |
|||
compareKey, |
|||
run, |
|||
}) |
|||
|
|||
const reshardRecordsAction = diff => [ |
|||
action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords), |
|||
] |
|||
|
|||
const rebuildIndexAction = diff => [ |
|||
action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex), |
|||
] |
|||
|
|||
const newIndexAction = diff => { |
|||
if (isAncestorIndex(diff.newNode)) { |
|||
return [ |
|||
action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex), |
|||
] |
|||
} else { |
|||
return [action(diff, `newIndex-${diff.newNode.nodeKey()}`, runNewIndex)] |
|||
} |
|||
} |
|||
|
|||
const deleteIndexAction = diff => [ |
|||
action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex), |
|||
] |
|||
|
|||
const deleteRecordsAction = diff => [ |
|||
action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords), |
|||
] |
|||
|
|||
const renameRecordAction = diff => [ |
|||
action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord), |
|||
] |
|||
|
|||
const recordCreatedAction = diff => { |
|||
if (isTopLevelRecord(diff.newNode)) { |
|||
return [action(diff, `initialiseRoot`, runInitialiseRoot)] |
|||
} |
|||
|
|||
return [ |
|||
action( |
|||
diff, |
|||
`initialiseChildRecord-${diff.newNode.nodeKey()}`, |
|||
runInitialiseChildRecord |
|||
), |
|||
] |
|||
} |
|||
|
|||
const rebuildAffectedIndexesAction = diff => { |
|||
const newHierarchy = findRoot(diff.newNode) |
|||
const oldHierarchy = findRoot(diff.oldNode) |
|||
const indexes = getDependantIndexes(newHierarchy, diff.newNode) |
|||
|
|||
const changedFields = (() => { |
|||
const addedFields = differenceBy(f => f.name)(diff.oldNode.fields)( |
|||
diff.newNode.fields |
|||
) |
|||
|
|||
const removedFields = differenceBy(f => f.name)(diff.newNode.fields)( |
|||
diff.oldNode.fields |
|||
) |
|||
|
|||
return map(f => f.name)([...addedFields, ...removedFields]) |
|||
})() |
|||
|
|||
const isIndexAffected = i => { |
|||
if ( |
|||
!isEqual(generateSchema(oldHierarchy, i), generateSchema(newHierarchy, i)) |
|||
) |
|||
return true |
|||
|
|||
if (some(f => indexes.filter.indexOf(`record.${f}`) > -1)(changedFields)) |
|||
return true |
|||
|
|||
if ( |
|||
some(f => indexes.getShardName.indexOf(`record.${f}`) > -1)(changedFields) |
|||
) |
|||
return true |
|||
|
|||
return false |
|||
} |
|||
|
|||
return $(indexes, [ |
|||
filter(isIndexAffected), |
|||
map(i => |
|||
action({ newNode: i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex) |
|||
), |
|||
]) |
|||
} |
|||
|
|||
const runReshardRecords = async change => { |
|||
throw new Error("Resharding of records is not supported yet") |
|||
} |
|||
|
|||
const runRebuildIndex = async (_, newApp, diff) => { |
|||
await _buildIndex(newApp, diff.newNode.nodeKey()) |
|||
} |
|||
|
|||
const runDeleteIndex = async (oldApp, _, diff) => { |
|||
await deleteAllIndexFilesForNode(oldApp, diff.oldNode) |
|||
} |
|||
|
|||
const runDeleteRecords = async (oldApp, _, diff) => { |
|||
await deleteAllRecordsForNode(oldApp, diff.oldNode) |
|||
} |
|||
|
|||
const runNewIndex = async (_, newApp, diff) => { |
|||
await initialiseNewIndex(newApp, diff.newNode) |
|||
} |
|||
|
|||
const runRenameRecord = change => { |
|||
/* |
|||
Going to disllow this in the builder. once a collection key is set... its done |
|||
*/ |
|||
} |
|||
|
|||
const runInitialiseRoot = async (_, newApp) => { |
|||
await initialiseData(newApp.datastore, newApp) |
|||
} |
|||
|
|||
const runInitialiseChildRecord = async (_, newApp, diff) => { |
|||
await initialiseChildrenForNode(newApp, diff.newNode) |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue