├── .eslintrc ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── bin └── firebase-bolt ├── docs ├── changelog.txt ├── guide.md ├── images │ └── flash.png ├── language.md └── todo.md ├── gulpfile.js ├── package.json ├── samples ├── all_access.bolt ├── all_access.json ├── chat.bolt ├── chat.json ├── children-by-nesting.bolt ├── children-by-nesting.json ├── children.bolt ├── children.json ├── create-update-delete.bolt ├── create-update-delete.json ├── functional.bolt ├── functional.json ├── generics.bolt ├── generics.json ├── groups.bolt ├── groups.json ├── issue-111.bolt ├── issue-111.json ├── issue-118.bolt ├── issue-118.json ├── issue-136.bolt ├── issue-136.json ├── issue-169.bolt ├── issue-169.json ├── issue-232.bolt ├── issue-232.json ├── issue-97.bolt ├── issue-97.json ├── mail.bolt ├── mail.json ├── map-scalar.bolt ├── map-scalar.json ├── multi-update.bolt ├── multi-update.json ├── regexp.bolt ├── regexp.json ├── serialized.bolt ├── serialized.json ├── type-extension.bolt ├── type-extension.json ├── user-security.bolt ├── user-security.json ├── userdoc.bolt └── userdoc.json ├── src ├── ast.ts ├── bolt.ts ├── file-io.ts ├── firebase-rest.ts ├── logger.ts ├── parse-util.ts ├── rules-generator.ts ├── rules-parser.pegjs ├── simulator.ts ├── test │ ├── ast-test.ts │ ├── chat-test.ts │ ├── cli-test.ts │ ├── create-update-delete-test.ts │ ├── firebase-rest-test.ts │ ├── firebase-rules-test.ts │ ├── generator-test.ts │ ├── index.html │ ├── issue-118-test.ts │ ├── mail-test.ts │ ├── parser-test.ts │ ├── regexp-test.ts │ ├── sample-files.ts │ ├── test-helper.ts │ └── util-test.ts └── util.ts ├── tools ├── bash-helper ├── browser-tests ├── compare-sample ├── configure-project ├── debug-tests ├── ensure-secret.py ├── flatten-json.py ├── regenerate-sample-files ├── run-tests ├── use ├── usefuncs └── web-server ├── tsconfig.json ├── tslint.json ├── typings.json └── typings ├── browser.d.ts ├── browser └── ambient │ ├── chai │ └── chai.d.ts │ ├── es6-promise │ └── es6-promise.d.ts │ ├── mocha │ └── mocha.d.ts │ └── node │ └── node.d.ts ├── main.d.ts └── main └── ambient ├── chai └── chai.d.ts ├── es6-promise └── es6-promise.d.ts ├── mocha └── mocha.d.ts └── node └── node.d.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "globals": { 8 | "define" : false 9 | }, 10 | "rules": { 11 | /** 12 | * Strict mode 13 | */ 14 | "strict": [0, "global"], // http://eslint.org/docs/rules/strict 15 | 16 | /** 17 | * Variables 18 | */ 19 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 20 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 21 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 22 | "vars": "local", 23 | "args": "none" 24 | }], 25 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define 26 | 27 | /** 28 | * Node.js 29 | */ 30 | "no-process-exit": 0, // http://eslint.org/docs/rules/no-process-exit 31 | "no-sync": 2, // http://eslint.org/docs/rules/no-sync 32 | 33 | /** 34 | * Possible errors 35 | */ 36 | "comma-dangle": [0, "never"], // http://eslint.org/docs/rules/comma-dangle 37 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 38 | "no-console": 0, // http://eslint.org/docs/rules/no-console 39 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 40 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 41 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 42 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 43 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 44 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 45 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 46 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 47 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 48 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 49 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 50 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 51 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 52 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 53 | "no-reserved-keys": 0, // http://eslint.org/docs/rules/no-reserved-keys 54 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 55 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 56 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 57 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 58 | 59 | /** 60 | * Best practices 61 | */ 62 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 63 | "curly": [2, "all"], // http://eslint.org/docs/rules/curly 64 | "default-case": 0, // http://eslint.org/docs/rules/default-case 65 | "dot-notation": [0, { // http://eslint.org/docs/rules/dot-notation 66 | "allowKeywords": true 67 | }], 68 | "eqeqeq": [0, "smart"], // http://eslint.org/docs/rules/eqeqeq 69 | "guard-for-in": 0, // http://eslint.org/docs/rules/guard-for-in 70 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 71 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 72 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 73 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 74 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 75 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 76 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 77 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 78 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 79 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 80 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 81 | "no-multi-str": 0, // http://eslint.org/docs/rules/no-multi-str 82 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 83 | "no-new": 0, // http://eslint.org/docs/rules/no-new 84 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 85 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 86 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 87 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 88 | //"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 89 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 90 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 91 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 92 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 93 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 94 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 95 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 96 | "no-with": 2, // http://eslint.org/docs/rules/no-with 97 | "radix": 2, // http://eslint.org/docs/rules/radix 98 | //"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 99 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 100 | "yoda": 2, // http://eslint.org/docs/rules/yoda 101 | 102 | /** 103 | * Style 104 | */ 105 | "indent": [2, 2], // http://eslint.org/docs/rules/indent 106 | "max-len": [2, 100, 2], 107 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 108 | "1tbs", { 109 | "allowSingleLine": true 110 | }], 111 | "quotes": [ 112 | 0, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 113 | ], 114 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 115 | "properties": "never" 116 | }], 117 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 118 | "before": false, 119 | "after": true 120 | }], 121 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 122 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 123 | //"func-names": 1, // http://eslint.org/docs/rules/func-names 124 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 125 | "beforeColon": false, 126 | "afterColon": true 127 | }], 128 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 129 | "newIsCap": true 130 | }], 131 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 132 | "max": 2 133 | }], 134 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 135 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 136 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 137 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 138 | "no-wrap-func": 0, // http://eslint.org/docs/rules/no-wrap-func 139 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 140 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 141 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 142 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 143 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 144 | "before": false, 145 | "after": true 146 | }], 147 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 148 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 149 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 150 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 151 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 152 | "spaced-line-comment": 0, // http://eslint.org/docs/rules/spaced-line-comment 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | bower_components/ 4 | node_modules/ 5 | lib/ 6 | dist/ 7 | tmp/ 8 | npm-debug.log 9 | auth-secrets.js 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - '0.10' 5 | - '0.12' 6 | sudo: false 7 | install: 8 | - npm install 9 | script: gulp 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Firebase Bolt authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as: 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | Google Inc. 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | 6 | Before we can use your code, you must sign the [Google Individual Contributor 7 | License Agreement](https://cla.developers.google.com/about/google-individual) 8 | (CLA), which you can do online. The CLA is necessary mainly because you own the 9 | copyright to your changes, even after your contribution becomes part of our 10 | codebase, so we need your permission to use and distribute your code. We also 11 | need to be sure of various other things—for instance that you'll tell us if you 12 | know that your code infringes on other people's patents. You don't have to sign 13 | the CLA until after you've submitted your code for review and a member has 14 | approved it, but you must do it before we can put your code into our codebase. 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | 22 | All submissions, including submissions by project members, require review. We 23 | use Github pull requests for this purpose. 24 | 25 | ### The small print 26 | 27 | Contributions made by corporations are covered by a different agreement than the 28 | one above, the [Software Grant and Corporate Contributor License 29 | Agreement](https://cla.developers.google.com/about/google-corporate). 30 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # People who have agreed to one of the CLAs and can contribute patches. 2 | # The AUTHORS file lists the copyright holders; this file 3 | # lists people. For example, Google employees are listed here 4 | # but not in AUTHORS, because Google holds the copyright. 5 | # 6 | # https://developers.google.com/open-source/cla/individual 7 | # https://developers.google.com/open-source/cla/corporate 8 | # 9 | # Names should be added to this file as: 10 | # Name 11 | 12 | Mike Koss 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Bolt Icon](docs/images/flash.png) Bolt Compiler 2 | 3 | [![Build Status](https://travis-ci.org/FirebaseExtended/bolt.svg?branch=master)](https://travis-ci.org/FirebaseExtended/bolt) 4 | [![NPM Version](https://badge.fury.io/js/firebase-bolt.svg)](https://npmjs.org/package/firebase-bolt) 5 | [![NPM Downloads](http://img.shields.io/npm/dm/firebase-bolt.svg)](https://npmjs.org/package/firebase-bolt) 6 | 7 | Bolt is an experimental security and rules compiler for Firebase Realtime Database (not for Firebase Cloud Storage). 8 | It is currently in beta. The language definition is converging, but not yet finalized. We welcome 9 | experimentation, but ask that you hand-verify the resulting JSON output before 10 | using with production applications. 11 | 12 | Otherwise, we'd love to have feedback from early adopters. You can email questions 13 | to firebase-talk@googlegroups.com using "Bolt" in the subject line, or post bugs 14 | on our [Issue Tracker](https://github.com/FirebaseExtended/bolt/issues). 15 | 16 | 17 | ## Status 18 | 19 | ![Status: Frozen](https://img.shields.io/badge/Status-Frozen-yellow) 20 | 21 | This repository is no longer under active development. No new features will be added and issues are not actively triaged. Pull Requests which fix bugs are welcome and will be reviewed on a best-effort basis. 22 | 23 | If you maintain a fork of this repository that you believe is healthier than the official version, we may consider recommending your fork. Please open a Pull Request if you believe that is the case. 24 | 25 | 26 | # Language Definition 27 | 28 | - [Guide to Using Firebase Bolt](docs/guide.md) - Introduction to using Bolt. 29 | - [Firebase Security and Modeling Language](docs/language.md) - Language documentation and syntax. 30 | 31 | # Using the Bolt Compiler 32 | 33 | You can easily install the bolt compiler using [npm](https://docs.npmjs.com/cli/install): 34 | 35 | $ npm install --global firebase-bolt 36 | 37 | Execute the Bolt compiler from the command line: 38 | 39 | $ firebase-bolt rules.bolt 40 | 41 | Will create a rules.json which you can then upload via the [Firebase Web Console](https://console.firebase.google.com/) 42 | or the [Firebase command 43 | line](https://firebase.google.com/docs/cli): 44 | 45 | $ firebase deploy 46 | 47 | _The firebase command line tool version 2 will also compile your Bolt file directly if you have firebase-bolt 48 | installed and you use the .bolt file extension in the rules property of your firebase.json 49 | configuration file._ 50 | 51 | # Developing with this Repo 52 | 53 | You should have node.js and npm installed to use this repository. 54 | 55 | Setup command line environment and build and test. 56 | 57 | $ source tools/use 58 | $ configure-project 59 | $ gulp 60 | 61 | # Useful commands 62 | 63 | Check JavaScript source files for required formatting conventions: 64 | 65 | $ gulp lint 66 | 67 | Build Bolt parser from PEG grammar: 68 | 69 | $ gulp build 70 | 71 | Run command line tests: 72 | 73 | $ gulp test 74 | 75 | More extensive tests which include running against a sandboxed Firebase app: 76 | 77 | $ run-tests 78 | 79 | Run browser-based tests: 80 | 81 | $ browser-tests 82 | -------------------------------------------------------------------------------- /bin/firebase-bolt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Copyright 2015 Google Inc. All Rights Reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 'use strict'; 19 | 20 | var fs = require('fs'); 21 | var parseArgs = require('minimist'); 22 | 23 | var util = require('../lib/util'); 24 | var bolt = require('../lib/bolt'); 25 | var pkg = require('../package.json'); 26 | var logger = require('../lib/logger'); 27 | 28 | var VERSION_STRING = "Firebase Bolt v" + pkg.version; 29 | var DEBUG = false; 30 | 31 | var opts = { 32 | boolean: ['version', 'help', 'debug'], 33 | string: ['output'], 34 | alias: { 35 | 'v': 'version', 36 | 'h': 'help', 37 | 'o': 'output', 38 | 'd': 'debug' 39 | }, 40 | unknown: function(flag) { 41 | if (flag[0] == '-') { 42 | log("Unknown flag: '" + flag + "'"); 43 | usage(1); 44 | } 45 | } 46 | }; 47 | 48 | function main() { 49 | var args = parseArgs(process.argv.slice(2), opts); 50 | 51 | // Option present - but missing value. 52 | if (args.output == '') { 53 | log("Missing output file name."); 54 | process.exit(1); 55 | } 56 | 57 | if (args.version) { 58 | console.log(VERSION_STRING); 59 | return; 60 | } 61 | 62 | if (args.debug) { 63 | DEBUG = true; 64 | logger.setDebug(); 65 | } 66 | 67 | if (args.help) { 68 | usage(0); 69 | } 70 | 71 | if (args._.length > 1) { 72 | log("Can only compile a single file."); 73 | usage(1); 74 | } 75 | 76 | // Read Bolt file from stdin 77 | if (args._.length === 0) { 78 | if (process.stdin.isTTY) { 79 | log("Type in a Bolt file terminated by CTRL-D."); 80 | } 81 | readFile(process.stdin, function(data) { 82 | if (args.output !== undefined) { 83 | writeTranslation(util.ensureExtension(args.output, 'json'), data); 84 | } else { 85 | console.log(translateRules(data)); 86 | } 87 | }); 88 | return; 89 | } 90 | 91 | // Read Bolt file and write json file. 92 | var inFile = util.ensureExtension(args._[0], bolt.FILE_EXTENSION); 93 | var outFile; 94 | if (args.output) { 95 | outFile = util.ensureExtension(args.output, 'json'); 96 | } else { 97 | outFile = util.replaceExtension(inFile, 'json'); 98 | } 99 | if (inFile === outFile) { 100 | log("Cannot overwrite input file: " + inFile); 101 | log("(Did you mean '" + util.replaceExtension(inFile, 'bolt') + "'?)"); 102 | process.exit(1); 103 | } 104 | fs.readFile(inFile, 'utf8', function(err, data) { 105 | if (err) { 106 | log("Could not read file: " + inFile); 107 | process.exit(1); 108 | } 109 | writeTranslation(outFile, data); 110 | }); 111 | } 112 | 113 | function writeTranslation(outFile, data) { 114 | log("Generating " + outFile + "..."); 115 | fs.writeFile(outFile, translateRules(data) + '\n', 'utf8', function(err2) { 116 | if (err2) { 117 | log("Could not write file: " + outFile); 118 | process.exit(1); 119 | } 120 | }); 121 | } 122 | 123 | function usage(code) { 124 | var cmdName = process.argv[1].split('/').slice(-1); 125 | console.error("Translate Firebase Bolt file into JSON rules format.\n"); 126 | 127 | console.error(" Usage: " + cmdName + " [options] [file]\n"); 128 | 129 | console.error(" Examples: " + cmdName + " myapp.bolt --output rules.json"); 130 | console.error(" " + cmdName + " < myapp.bolt > rules.json"); 131 | console.error(" " + cmdName + " myapp"); 132 | console.error(" (myapp.bolt => myapp.json)\n"); 133 | 134 | console.error(" Options:\n"); 135 | console.error(util.formatColumns(4, [ 136 | ["-h --help", "Display this helpful message."], 137 | ["-o --output file", "Output to file.json."], 138 | ["-v --version", "Display Firebase Bolt version."], 139 | ["-d --debug", "Display additional debugging information."], 140 | [] 141 | ]).join('\n')); 142 | 143 | process.exit(code); 144 | } 145 | 146 | main(); 147 | 148 | function readFile(f, callback) { 149 | var input = ""; 150 | 151 | f.setEncoding('utf8'); 152 | f.on('data', function(chunk) { 153 | input += chunk; 154 | }); 155 | 156 | f.on('end', function() { 157 | callback(input); 158 | }); 159 | } 160 | 161 | function translateRules(input) { 162 | var symbols; 163 | var rules; 164 | 165 | try { 166 | symbols = bolt.parse(input); 167 | } catch (e) { 168 | if (DEBUG) { 169 | log(e.stack); 170 | } 171 | log(e.message, e.line, e.column); 172 | process.exit(1); 173 | } 174 | 175 | try { 176 | var gen = new bolt.Generator(symbols); 177 | rules = gen.generateRules(); 178 | } catch (e) { 179 | if (DEBUG) { 180 | log(e.stack); 181 | } 182 | log(e.message); 183 | process.exit(2); 184 | } 185 | 186 | return JSON.stringify(rules, null, 2); 187 | } 188 | 189 | function log(message, line, column) { 190 | var parts = ['bolt']; 191 | if (line) { 192 | util.extendArray(parts, [line, column]); 193 | } 194 | parts.push(' ' + message); 195 | console.error(parts.join(':')); 196 | } 197 | -------------------------------------------------------------------------------- /docs/changelog.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/bolt/a0a3003261123ecb6cbff878aa6d2f341f574725/docs/changelog.txt -------------------------------------------------------------------------------- /docs/images/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirebaseExtended/bolt/a0a3003261123ecb6cbff878aa6d2f341f574725/docs/images/flash.png -------------------------------------------------------------------------------- /docs/language.md: -------------------------------------------------------------------------------- 1 | # Firebase Bolt Security and Modeling Language 2 | 3 | This language is meant to be used as a convenient front-end to the existing 4 | Firebase JSON-based rules language. 5 | 6 | # File Structure 7 | 8 | A bolt file consists of 3 types of statements: 9 | 10 | - Types: Definition of an object schema. 11 | - Paths: Definition of storage locations, and what type of accesses are allowed there. 12 | - Functions: Global function expressions which can be called from other expressions 13 | to be used as helpers. 14 | 15 | A bolt file can also contain JavaScript-style comments: 16 | 17 | ```javascript 18 | // Single line comment 19 | 20 | /* Multi 21 | line 22 | comment 23 | */ 24 | ``` 25 | 26 | # Types 27 | 28 | A (user-defined) type statement describes a value that can be stored in the Firebase database. 29 | 30 | ```javascript 31 | type MyType [extends BaseType] { 32 | property1: Type, 33 | property2: Type, 34 | ... 35 | 36 | validate() { } 37 | } 38 | } 39 | ``` 40 | 41 | If the `validate` expression is `false`, then the value is deemed to be invalid and cannot 42 | be saved to the database (an error will be returned to the Firebase client). 43 | 44 | Within the `validate` expression, the special value `this` references the object 45 | of type, `MyType`. Properties of `MyType` can be referenced in expressions like 46 | `this.property1`. 47 | 48 | Types can extend other types by using the `extends` clause. If not given, 49 | `Object` is assumed when `MyType` has child properties (or `Any` if it does not). Types 50 | which extend an Object, can add additional properties to the Object, in addition to 51 | a `validate` expression. 52 | 53 | Property names in type statements should be valid Identifiers (see below). If you need 54 | to use any other character in a property name, you can enclose them in quotes (note 55 | that Firebase allows any character in a path *except* for `.`, `$`, `#`, `[`, `[`, `/`, 56 | or control characters). 57 | 58 | Built-in base types are also similar to JavaScript types: 59 | 60 | String - Character strings 61 | Number - Integer or floating point 62 | Boolean - Values `true` or `false` 63 | Object - A structured object containing named properties. 64 | Any - Every non-null value is of type Any. 65 | Null - Value `null` (same as absence of a value, or deleted) 66 | Map - A generic type - maps string valued keys to corresponding 67 | values (similar to an Object type). 68 | Type[] - An "array-like" type (actually same as Map 69 | where Type can be any other built-in or user-defined type. 70 | 71 | Any of the built-in scalar types can be _extended_ by adding a validation expression, e.g.: 72 | 73 | ```javascript 74 | type ShortString extends String { 75 | validate() { this.length < 32 } 76 | } 77 | 78 | type Percentage extends Number { 79 | validate() { this >=0 && this <= 100 } 80 | } 81 | ``` 82 | 83 | Notes: 84 | 85 | - Object types are required to have at least one property when present. 86 | - Map types can be empty collections (they need not contain any child keys). 87 | 88 | ## Type Expressions 89 | 90 | Any place a Type can be used, it can be replaced with a Type expression. We support 91 | three types of type expressions: 92 | 93 | ### Union Types 94 | 95 | Type1 | Type2 - Value can be either of two types. 96 | Type | Null - An optional `Type` value (value can be deleted or missing). 97 | 98 | 99 | ### Map Types (Collections) 100 | 101 | A Map type is a built-in Generic Type (see below). It is used to specify collections 102 | within a model: 103 | 104 | ```javascript 105 | type Model { 106 | users: Map, 107 | products: Map 108 | } 109 | 110 | type ProductID extends String { 111 | validate() { this.length <= 20 } 112 | } 113 | ``` 114 | 115 | As a shortcut for the common `Map`, "array-like" notation can be used: 116 | 117 | ```javascript 118 | type Model { 119 | users: User[], 120 | products: Product[] 121 | } 122 | ``` 123 | 124 | ### Generic Types 125 | 126 | A generic type is like a "type macro" - it is used to specify a type generically, 127 | but then make it specific to a particular use case: 128 | 129 | ```javascript 130 | type Pair { 131 | first: X, 132 | second: Y 133 | } 134 | ``` 135 | 136 | Note that the types of the `first` and `second` properties uses the placeholder types, 137 | `X` and `Y`. Using a generic type is much like a function call - except using `<...>` instead 138 | of `(...)`: 139 | 140 | ```javascript 141 | type Model { 142 | name: String, 143 | prop: Pair; 144 | } 145 | ``` 146 | 147 | # Paths 148 | 149 | A path statement provides access and validation rules for data stored at a given path. 150 | 151 | ```javascript 152 | path /path/to/data [is Type] { 153 | read() { } 154 | 155 | write() { } 156 | 157 | validate() { } 158 | } 159 | ``` 160 | 161 | If a Type is not given, `Any` is assumed. 162 | 163 | In `read` expressions, the value of `this` is the value stored at the path of 164 | type, `Type`. In `write` and `validate` expressions `this` is the value to be 165 | stored at the path (use the `prior(this)` function to reference the previously stored 166 | value of `this`). 167 | 168 | The `read` and `write` expressions are used to determine when users are allowed 169 | to read or modify the data at the given path. These rules typically test the 170 | value of the global `auth` variable and possibly reference other locations of the 171 | database to determine these permissions. 172 | 173 | The `validate` expression can be used to check for additional constraints 174 | (beyond the Type `validate` rules) required to store a value at the given path, 175 | and especially perform constraints that are path-dependent. Path 176 | templates can include _captured_ parts whose values can then be used within 177 | an expression as a variable parameter: 178 | 179 | ```javascript 180 | path /users/{uid} is User { 181 | // Anyone can read a User's information. 182 | read() { true } 183 | 184 | // Only an authenticated user can write their information. 185 | write() { auth != null && auth.uid == uid } 186 | } 187 | ``` 188 | 189 | If a path needs no expressions, the following abbreviated form (without a body) 190 | can be used: 191 | 192 | path /users/{uid} is User; 193 | 194 | and the `path` keyword can also be omitted. 195 | 196 | /users/{uid} is User; 197 | 198 | Paths statments can be nested: 199 | 200 | ```javascript 201 | path /users { 202 | // Anyone can read the list of users and read a their information. 203 | read() { true } 204 | 205 | /{uid} is User { 206 | // Authenticated user can write their own information. 207 | write() { auth != null && auth.uid == uid } 208 | } 209 | } 210 | ``` 211 | 212 | 213 | ## Write Aliases 214 | 215 | A common pattern is to have distinct rules for allowing writes to a location that represent, 216 | new object creation (create), modification of existing data (update), or deleting data (delete). 217 | Bolt allows you to use these methods in lieu of the write() method in any path or type 218 | statement. 219 | 220 | Alias | Write Equivalent 221 | -----------------| ---------------- 222 | create() { exp } | write() { prior(this) == null && exp } 223 | update() { exp } | write() { prior(this) != null && this != null && exp } 224 | delete() { exp } | write() { prior(this) != null && this == null && exp } 225 | 226 | If you use any of create(), update(), or delete(), you may not use a write() method in your 227 | path or type statement. 228 | 229 | 230 | ## String methods 231 | 232 | The following methods can be used on string (static valued or strings stored 233 | in the database): 234 | 235 | s.length - Number of characters in the string. 236 | s.includes(sub) - Returns true iff sub is a substring of s. 237 | s.startsWith(sub) - Returns true iff sub is a prefix of s. 238 | s.endsWith(sub) - Returns true iff sub is a suffix of s. 239 | s.replace(old, new) - Returns a string where all occurances of string, `old`, are 240 | replaced by `new`. 241 | s.toLowerCase() - Returns an all lower case version of s. 242 | s.toUpperCase() - Returns an all upper case version of s. 243 | s.test(regexp) - Returns true iff the string matches the regular expression. 244 | 245 | [Regular Expression Syntax](https://www.firebase.com/docs/security/api/string/matches.html) 246 | 247 | ## Database references 248 | 249 | References to data locations (starting with `this` or `root`) can be further qualified 250 | using the `.` and `[]` operators (just as in JavaScript Object references). 251 | 252 | ref.child - Returns the property `child` of the reference. 253 | ref[s] - Return property referenced by the string, variable, `s`. 254 | ref.parent() - Returns the parent of the given refererence 255 | (e.g., ref.prop.parent() is the same as ref). 256 | 257 | To reference the previous value of a property (in a write() or validate() rule), use 258 | the `prior()` function: 259 | 260 | prior(this) - Value of `this` before the write is completed. 261 | prior(this.prop) - Value of a property before the write is completed. 262 | 263 | `prior()` can be used to wrap any expressions (including function calls) that 264 | use `this`. 265 | 266 | The parent key of the current location can be read using the key() function. 267 | 268 | key() - The (text) value of the inner-most parent property of the current location. 269 | 270 | This can be used to create a validation expression that relates the key used to store a value 271 | and one of its properties: 272 | 273 | ```javascript 274 | path /products is Product[]; 275 | 276 | type Product { 277 | validate() { this.id == key() } 278 | 279 | id: String, 280 | name: String 281 | } 282 | 283 | ``` 284 | 285 | 286 | # Functions and Methods 287 | 288 | Functions must be simple return expressions with zero or more parameters. All of the following 289 | examples are identical and can be used interchangably. 290 | 291 | ```javascript 292 | function isUser(uid) { 293 | return auth != null && auth.uid == uid; 294 | } 295 | 296 | function isUser(uid) { auth != null && auth.uid == uid } 297 | 298 | isUser(uid) { auth != null && auth.uid == uid } 299 | ``` 300 | 301 | Similarly, methods in path and type statements can use the abbreviated functional form (all 302 | these are equivalent): 303 | 304 | ```javascript 305 | write() { return this.user == auth.uid; } 306 | write() { this.user == auth.uid; } 307 | write() { this.user == auth.uid } 308 | ``` 309 | 310 | # Identifiers 311 | 312 | Identifiers in expressions, property names, and path captured parts, must begin with one of 313 | alphabetic, _ or $ characters and can contain any alphabetic, numeric, _ or $. 314 | 315 | # Expressions 316 | 317 | Rule expressions are a subset of JavaScript expressions, and include: 318 | 319 | - Unary operators: - (minus), ! (boolean negation) 320 | - Binary operators: +, -, *, /, % 321 | - String constants can be expressed using single or double quotes and can 322 | include Hex escape characters (\xXX), Unicode escape characters (\uXXXX) 323 | or special escape characters \b, \f, \n, \r, or \t. 324 | 325 | # Global variables 326 | 327 | These global variables are available in expressions: 328 | 329 | root - The root location of a Firebase database. 330 | auth - The current auth state (if auth != null the user is authenticated, and auth.uid 331 | is their user identifier string). 332 | now - The (Unix) timestamp of the current time (a Number). 333 | 334 | # Appendix A. Firebase Expressions and their Bolt equivalents. 335 | 336 | The special [Security and Rules API](https://www.firebase.com/docs/security/api/) in Firebase is 337 | not identical in Bolt. This section demonstrates how equivalent behavior is achieved in Bolt. 338 | 339 | ## Rules 340 | 341 | API | Bolt Equivalent 342 | ----| --------------- 343 | ".read" : "exp" | read() { exp } 344 | ".write" : "exp" | write() { exp } 345 | ".validate": "exp" | validate() { exp } 346 | ".indexOn": [ "prop", ...] | index() { [ "prop", ... ] } 347 | 348 | ## Variables 349 | 350 | API | Bolt Equivalent 351 | ----| --------------- 352 | auth | auth 353 | $location | {location} (in path statement) 354 | now | now 355 | data | prior(this) 356 | newData | this (in validate() and write() rules) 357 | 358 | ## RuleDataSnapshot Methods 359 | 360 | API | Bolt Equivalent 361 | ----| --------------- 362 | ref.val() | Implicit. Just use ref in an expression (e.g., ref > 0). 363 | ref.child('prop') | ref.prop 364 | ref.child(exp) | ref[exp] 365 | ref.parent() | ref.parent() 366 | ref.hasChild(prop) | ref.prop != null 367 | ref.hasChildren(props) | implicit using "path is Type" 368 | ref.exists() | ref != null 369 | ref.getPriority() | Not Supported 370 | ref.isNumber() | prop: Number 371 | ref.isString() | prop: String 372 | ref.isBoolean() | prop: Boolean 373 | 374 | ## String Methods 375 | 376 | API | Bolt Equivalent 377 | ----| --------------- 378 | s.length | s.length 379 | s.contains(sub) | s.includes(sub) 380 | s.beginsWith(sub) | s.startsWith(sub) 381 | s.endsWith(sub) | s.endsWith(sub) 382 | s.replace(old, new) | s.replace(old, new) 383 | s.matches(/reg/) | s.test(/reg/) 384 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | # Known issues and tasks. 2 | 3 | - Offline storage simulator (fake) 4 | - Enable online and offline tests for rules simulator 5 | - Make offline rest client fake 6 | 7 | # Language changes 8 | 9 | - Generate (Java) language stubs based on schema (cf bolt_compiler experiment). 10 | 11 | # Testing 12 | 13 | - Concept of "coverage" for behavioral tests? (Like ensure every property that 14 | can be testing against is tested in success and failing tests, and that every clause 15 | of a validation rule is executed in both the true and false cases). 16 | 17 | 18 | # Repo structure and OSS 19 | 20 | - Setup code coverage (istanbul). 21 | - Remove browser-test and web-server bash scripts (replace in gulp). 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | var argv = require('yargs').argv; 19 | var path = require('path'); 20 | var gulp = require('gulp'); 21 | var source = require('vinyl-source-stream'); 22 | var browserify = require('browserify'); 23 | var mkdirp = require('mkdirp'); 24 | var del = require('del'); 25 | var merge = require('merge-stream'); 26 | 27 | var eslint = require('gulp-eslint'); 28 | var tslint = require('gulp-tslint'); 29 | var mocha = require('gulp-mocha'); 30 | var gutil = require('gulp-util'); 31 | var ts = require('gulp-typescript'); 32 | var sourcemaps = require('gulp-sourcemaps'); 33 | var peg = require('gulp-peg'); 34 | var mustache = require('gulp-mustache'); 35 | var rename = require('gulp-rename'); 36 | 37 | var LIB_DIR = 'lib'; 38 | var TEST_DIR = path.join(LIB_DIR, 'test'); 39 | var DIST_DIR = 'dist'; 40 | var TMP_DIR = 'tmp'; 41 | 42 | var JS_SOURCES = ['gulpfile.js', 'bin/firebase-bolt']; 43 | var TS_SOURCES = ['src/*.ts', 'src/test/*.ts']; 44 | 45 | var COMMON_TESTS = ['ast', 'generator', 'parser', 'util']; 46 | var NETWORK_TESTS = ['firebase-rest', 'firebase-rules', 'chat', 'mail', 'regexp']; 47 | var CI_TESTS = COMMON_TESTS.concat(['cli']); 48 | var BROWSER_TESTS = COMMON_TESTS.concat(NETWORK_TESTS); 49 | 50 | var TEST_SETS = [ 51 | { title: "Common Tests", 52 | filename: 'index.html', 53 | tests: COMMON_TESTS }, 54 | { title: "Network Tests", 55 | filename: 'network.html', 56 | tests: NETWORK_TESTS }, 57 | { title: "All Tests", 58 | filename: 'all.html', 59 | tests: BROWSER_TESTS } 60 | ]; 61 | 62 | gulp.task('clean', function(cb) { 63 | del([LIB_DIR, DIST_DIR, TMP_DIR], cb); 64 | }); 65 | 66 | gulp.task('eslint', function() { 67 | return gulp.src(JS_SOURCES) 68 | .pipe(eslint()) 69 | .pipe(eslint.format()) 70 | .pipe(eslint.failAfterError()); 71 | }); 72 | 73 | gulp.task('tslint', function() { 74 | return gulp.src(TS_SOURCES) 75 | .pipe(tslint()) 76 | .pipe(tslint.report('prose')); 77 | }); 78 | 79 | gulp.task('lint', ['eslint', 'tslint']); 80 | 81 | gulp.task('ts-compile', ['build-peg'], function() { 82 | var tsProject = ts.createProject('tsconfig.json', { 83 | // Use the repo-installed version of typescript instead of 84 | // the version built into gulp-typescript. 85 | typescript: require('typescript') 86 | }); 87 | var tsResult = tsProject.src() 88 | .pipe(sourcemaps.init()) 89 | .pipe(ts(tsProject)); 90 | 91 | return merge([ 92 | tsResult.pipe(sourcemaps.write()).pipe(gulp.dest(LIB_DIR)), 93 | tsResult.dts.pipe(gulp.dest(LIB_DIR)), 94 | ]); 95 | }); 96 | 97 | gulp.task('build', ['ts-compile', 'browserify-bolt']); 98 | 99 | gulp.task('build-peg', function() { 100 | return gulp.src('src/rules-parser.pegjs') 101 | .pipe(peg()) 102 | .pipe(gulp.dest(LIB_DIR)); 103 | }); 104 | 105 | gulp.task('build-browser-tests', ['browserify-tests'], function() { 106 | return merge(TEST_SETS.map(function(set) { 107 | // Mark the current set as selected for CSS class in test template 108 | var sets = TEST_SETS.map(function(otherSet) { 109 | if (set === otherSet) { 110 | return extend({}, otherSet, { selected: 'selected' }); 111 | } 112 | return otherSet; 113 | }); 114 | 115 | return gulp.src('src/test/index.html') 116 | .pipe(mustache({ sets: sets, set: set })) 117 | .pipe(rename(set.filename)) 118 | .pipe(gulp.dest(DIST_DIR)); 119 | })); 120 | }); 121 | 122 | gulp.task('browserify-tests', ['build'], function() { 123 | return merge(BROWSER_TESTS.map(testFileSource).map(browserifyToDist)); 124 | }); 125 | 126 | gulp.task('browserify-bolt', ['ts-compile'], function() { 127 | return browserifyToDist(path.join(LIB_DIR, 'bolt.js')); 128 | }); 129 | 130 | // Runs the Mocha test suite 131 | gulp.task('test', ['lint', 'build'], function() { 132 | mkdirp(TMP_DIR); 133 | var mochaOptions = { 134 | ui: 'tdd', 135 | require: ['source-map-support/register'] 136 | }; 137 | if (argv.grep) { 138 | mochaOptions['grep'] = argv.grep; 139 | } 140 | return gulp.src(CI_TESTS.map(testFileSource)) 141 | .pipe(mocha(mochaOptions)); 142 | }); 143 | 144 | gulp.task('default', ['test']); 145 | 146 | // Don't depend on 'build' in case current state is failing to compile - need to edit file 147 | // to kick off first watch build. 148 | gulp.task('watch', function() { 149 | gulp.watch(['src/*', 'src/test/*'], ['default']); 150 | }); 151 | 152 | gulp.task('watch-build', function() { 153 | gulp.watch(['src/*', 'src/test/*'], ['build', 'lint']); 154 | }); 155 | 156 | function browserifyToDist(entry, opts) { 157 | // Browserify options include: 158 | // standalone: name of exported module 159 | // debug: Include sourcemap in output. 160 | opts = extend({ entries: [entry], debug: true }, opts); 161 | var b = browserify(opts); 162 | return b 163 | .bundle() 164 | .pipe(source(path.basename(entry))) 165 | .on('error', gutil.log) 166 | .pipe(gulp.dest(DIST_DIR)); 167 | } 168 | 169 | function extend(dest) { 170 | var i; 171 | var src; 172 | var prop; 173 | 174 | if (dest === undefined) { 175 | dest = {}; 176 | } 177 | for (i = 1; i < arguments.length; i++) { 178 | src = arguments[i]; 179 | for (prop in src) { 180 | if (src.hasOwnProperty(prop)) { 181 | dest[prop] = src[prop]; 182 | } 183 | } 184 | } 185 | 186 | return dest; 187 | } 188 | 189 | function testFileSource(name) { 190 | return path.join(TEST_DIR, name + '-test.js'); 191 | } 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-bolt", 3 | "description": "Firebase Bolt Security and Modeling Language Compiler", 4 | "version": "0.8.4", 5 | "author": "Firebase (https://firebase.google.com/)", 6 | "main": "lib/bolt.js", 7 | "types": "lib/bolt.d.ts", 8 | "homepage": "https://github.com/firebase/bolt", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/firebase/bolt.git" 12 | }, 13 | "files": [ 14 | "lib", 15 | "bin", 16 | "dist", 17 | "LICENSE", 18 | "README.md", 19 | "package.json", 20 | "docs/language.md" 21 | ], 22 | "directories": { 23 | "bin": "bin", 24 | "doc": "docs" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/firebase/bolt/issues" 28 | }, 29 | "license": "Apache-2.0", 30 | "keywords": [ 31 | "firebase", 32 | "security", 33 | "rules", 34 | "schema", 35 | "models", 36 | "blaze" 37 | ], 38 | "preferGlobal": true, 39 | "scripts": { 40 | "test": "gulp test", 41 | "travis": "gulp" 42 | }, 43 | "devDependencies": { 44 | "browserify": "^11.0.1", 45 | "del": "^2.0.2", 46 | "eslint": "^1.1.0", 47 | "gulp": "^3.9.0", 48 | "gulp-eslint": "^1.0.0", 49 | "gulp-mocha": "^2.1.3", 50 | "gulp-mustache": "^2.2.0", 51 | "gulp-peg": "^0.1.2", 52 | "gulp-rename": "^1.2.2", 53 | "gulp-sourcemaps": "^1.5.2", 54 | "gulp-tslint": "^3.3.0-beta", 55 | "gulp-typescript": "^2.8.3", 56 | "gulp-util": "^3.0.6", 57 | "http-server": "^0.8.0", 58 | "merge-stream": "^1.0.0", 59 | "mkdirp": "^0.5.1", 60 | "mocha": "^2.2.5", 61 | "pegjs": "^0.8.0", 62 | "source-map-support": "^0.4.0", 63 | "tslint": "^2.5.0-beta", 64 | "typescript": "^2.0.0", 65 | "typings": "^1.3.2", 66 | "vinyl-source-stream": "^1.1.0", 67 | "yargs": "^3.32.0" 68 | }, 69 | "dependencies": { 70 | "chai": "^3.2.0", 71 | "es6-promise": "^3.2.1", 72 | "firebase-token-generator": "^2.0.0", 73 | "minimist": "^1.2.0", 74 | "node-uuid": "^1.4.3" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /samples/all_access.bolt: -------------------------------------------------------------------------------- 1 | // Equivalent to the default (most-permissive) Firebase rule. 2 | path / { 3 | read() { true } 4 | write() { true } 5 | } 6 | -------------------------------------------------------------------------------- /samples/all_access.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "true", 4 | ".write": "true" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/chat.bolt: -------------------------------------------------------------------------------- 1 | /* 2 | * Chat room application model. 3 | */ 4 | path /rooms is Map; 5 | path /posts/{roomid} { 6 | validate() { getRoomInfo(roomid) != null } 7 | } 8 | 9 | path /posts/{roomid}/{postid} is Timestamped { 10 | write() { isMember(getRoomInfo(roomid)) } 11 | } 12 | 13 | type RoomInfo { 14 | read() { isMember(this) || isUser(this.creator) } 15 | 16 | create() { true } 17 | update() { isUser(prior(this).creator) } 18 | delete() { isUser(prior(this).creator) } 19 | 20 | name: NameString, 21 | creator: UserID, 22 | members: Member[], 23 | } 24 | 25 | getRoomInfo(id) { prior(root).rooms[id] } 26 | 27 | isMember(roomInfo) { 28 | roomInfo.members[currentUser()] != null && !roomInfo.members[currentUser()].isBanned 29 | } 30 | 31 | type Post { 32 | // Allow create (but not modify/delete). 33 | create() { true } 34 | 35 | from: UserID, 36 | message: MessageString, 37 | } 38 | 39 | type MessageString extends String { 40 | validate() { this.length > 0 && this.length <= 140 } 41 | } 42 | 43 | type Member { 44 | // Anyone can add themselves to a Room with their own nickname. 45 | create() { isUser(key()) } 46 | 47 | nickname: NameString, 48 | isBanned: Boolean, 49 | } 50 | 51 | type NameString { 52 | validate() { this.length > 0 && this.length <= 32 } 53 | } 54 | 55 | type Timestamped extends T { 56 | created: Created, 57 | // modified: Modified, 58 | } 59 | 60 | type Created extends Number { 61 | validate() { initial(this, now) } 62 | } 63 | 64 | type Modified extends Number { 65 | validate() { this == now } 66 | } 67 | 68 | type PushID extends String { 69 | validate() { this.length == 20 } 70 | } 71 | 72 | type RoomID extends String { 73 | validate() { this.length >= 1 && this.length <= 32 } 74 | } 75 | 76 | type UserID extends String { 77 | // Only a user can create content with thier userid. 78 | validate() { isUser(this) } 79 | } 80 | 81 | isUser(uid) { auth != null && uid == currentUser() } 82 | currentUser() { auth.uid } 83 | 84 | initial(value, init) { value == (isInitial(value) ? init : prior(value)) } 85 | isInitial(value) { prior(value) == null } 86 | -------------------------------------------------------------------------------- /samples/chat.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "rooms": { 4 | "$key1": { 5 | ".validate": "((newData.hasChildren(['name', 'creator']) && $key1.length >= 1) && $key1.length <= 32)", 6 | "name": { 7 | ".validate": "(newData.val().length > 0 && newData.val().length <= 32)" 8 | }, 9 | "creator": { 10 | ".validate": "(newData.isString() && (auth != null && newData.val() == auth.uid))" 11 | }, 12 | "members": { 13 | "$key2": { 14 | ".validate": "newData.hasChildren(['nickname', 'isBanned'])", 15 | "nickname": { 16 | ".validate": "(newData.val().length > 0 && newData.val().length <= 32)" 17 | }, 18 | "isBanned": { 19 | ".validate": "newData.isBoolean()" 20 | }, 21 | "$other": { 22 | ".validate": "false" 23 | }, 24 | ".write": "(data.val() == null && (auth != null && $key2 == auth.uid))" 25 | }, 26 | ".validate": "newData.hasChildren()" 27 | }, 28 | "$other": { 29 | ".validate": "false" 30 | }, 31 | ".write": "((data.val() == null || ((data.val() != null && newData.val() != null) && (auth != null && data.child('creator').val() == auth.uid))) || ((data.val() != null && newData.val() == null) && (auth != null && data.child('creator').val() == auth.uid)))", 32 | ".read": "((data.child('members').child(auth.uid).val() != null && !(data.child('members').child(auth.uid).child('isBanned').val() == true)) || (auth != null && data.child('creator').val() == auth.uid))" 33 | }, 34 | ".validate": "newData.hasChildren()" 35 | }, 36 | "posts": { 37 | "$roomid": { 38 | ".validate": "root.child('rooms').child($roomid).val() != null", 39 | "$postid": { 40 | ".validate": "newData.hasChildren(['from', 'message', 'created'])", 41 | "from": { 42 | ".validate": "(newData.isString() && (auth != null && newData.val() == auth.uid))" 43 | }, 44 | "message": { 45 | ".validate": "((newData.isString() && newData.val().length > 0) && newData.val().length <= 140)" 46 | }, 47 | "$other": { 48 | ".validate": "false" 49 | }, 50 | ".write": "(data.val() == null && (root.child('rooms').child($roomid).child('members').child(auth.uid).val() != null && !(root.child('rooms').child($roomid).child('members').child(auth.uid).child('isBanned').val() == true)))", 51 | "created": { 52 | ".validate": "(newData.isNumber() && newData.val() == (data.val() == null ? now : data.val()))" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /samples/children-by-nesting.bolt: -------------------------------------------------------------------------------- 1 | type Child { 2 | age: Number 3 | } 4 | 5 | type Parent { 6 | children: Child[] 7 | } 8 | 9 | path /parents is Parent[] {} 10 | -------------------------------------------------------------------------------- /samples/children-by-nesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "parents": { 4 | "$key1": { 5 | ".validate": "newData.hasChildren()", 6 | "children": { 7 | "$key2": { 8 | ".validate": "newData.hasChildren(['age'])", 9 | "age": { 10 | ".validate": "newData.isNumber()" 11 | }, 12 | "$other": { 13 | ".validate": "false" 14 | } 15 | }, 16 | ".validate": "newData.hasChildren()" 17 | }, 18 | "$other": { 19 | ".validate": "false" 20 | } 21 | }, 22 | ".validate": "newData.hasChildren()" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/children.bolt: -------------------------------------------------------------------------------- 1 | type Child { 2 | age: Number 3 | } 4 | 5 | type Parent { 6 | } 7 | 8 | path /parents { 9 | /{parent} is Parent { 10 | /{child} is Child; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/children.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "parents": { 4 | "$parent": { 5 | "$child": { 6 | ".validate": "newData.hasChildren(['age'])", 7 | "age": { 8 | ".validate": "newData.isNumber()" 9 | }, 10 | "$other": { 11 | ".validate": "false" 12 | } 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/create-update-delete.bolt: -------------------------------------------------------------------------------- 1 | // Demonstrates how to create independent create, update, and delete write rules. 2 | 3 | path /docs/{uid} { 4 | /create { create() { true } } 5 | /update { update() { true } } 6 | /delete { delete() { true } } 7 | 8 | /create-or-update { 9 | create() { true } 10 | update() { true } 11 | } 12 | 13 | /owner-create-delete { 14 | create() { isOwner(uid) } 15 | delete() { isOwner(uid) } 16 | update() { true } 17 | } 18 | } 19 | 20 | isOwner(uid) { uid == auth.uid } 21 | -------------------------------------------------------------------------------- /samples/create-update-delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "docs": { 4 | "$uid": { 5 | "create": { 6 | ".write": "data.val() == null" 7 | }, 8 | "update": { 9 | ".write": "(data.val() != null && newData.val() != null)" 10 | }, 11 | "delete": { 12 | ".write": "(data.val() != null && newData.val() == null)" 13 | }, 14 | "create-or-update": { 15 | ".write": "(data.val() == null || (data.val() != null && newData.val() != null))" 16 | }, 17 | "owner-create-delete": { 18 | ".write": "(((data.val() == null && $uid == auth.uid) || (data.val() != null && newData.val() != null)) || ((data.val() != null && newData.val() == null) && $uid == auth.uid))" 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/functional.bolt: -------------------------------------------------------------------------------- 1 | function f(g, x) { return g(x)*3; } 2 | function y(x) { return x + 2; } 3 | 4 | path / { validate() { return f(y,this); }} 5 | -------------------------------------------------------------------------------- /samples/functional.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".validate": "(newData.val() + 2) * 3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /samples/generics.bolt: -------------------------------------------------------------------------------- 1 | path / is App { 2 | read() { true } 3 | } 4 | 5 | type App { 6 | users: Map, 7 | products: Map, 8 | } 9 | 10 | type User { 11 | name: String, 12 | age: Number 13 | } 14 | 15 | type Product { 16 | validate() { key() == this.id } 17 | 18 | id: ProductID, 19 | cost: Number 20 | } 21 | 22 | type ProductID extends String { 23 | validate() { this.length <= 20 } 24 | } 25 | 26 | type PushID extends String { 27 | validate() { this.length == 20 } 28 | } 29 | -------------------------------------------------------------------------------- /samples/generics.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".validate": "newData.hasChildren()", 4 | "users": { 5 | "$key1": { 6 | ".validate": "(newData.hasChildren(['name', 'age']) && $key1.length == 20)", 7 | "name": { 8 | ".validate": "newData.isString()" 9 | }, 10 | "age": { 11 | ".validate": "newData.isNumber()" 12 | }, 13 | "$other": { 14 | ".validate": "false" 15 | } 16 | }, 17 | ".validate": "newData.hasChildren()" 18 | }, 19 | "products": { 20 | "$key2": { 21 | ".validate": "((newData.hasChildren(['id', 'cost']) && $key2.length <= 20) && $key2 == newData.child('id').val())", 22 | "id": { 23 | ".validate": "(newData.isString() && newData.val().length <= 20)" 24 | }, 25 | "cost": { 26 | ".validate": "newData.isNumber()" 27 | }, 28 | "$other": { 29 | ".validate": "false" 30 | } 31 | }, 32 | ".validate": "newData.hasChildren()" 33 | }, 34 | "$other": { 35 | ".validate": "false" 36 | }, 37 | ".read": "true" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/groups.bolt: -------------------------------------------------------------------------------- 1 | // This example demonstrates how to make a "small collection" (limited 2 | // to no more than 10 items. It is also required to have at least one 3 | // member. 4 | 5 | path /groups/{gid} is Group; 6 | 7 | type Group { 8 | validate() { members != null } 9 | 10 | members: Map 11 | } 12 | 13 | type User { 14 | uid: String, 15 | name: String 16 | } 17 | 18 | type TenKeys extends String { 19 | validate() { this.length == 1 && this >= '0' && this <= '9' } 20 | } 21 | -------------------------------------------------------------------------------- /samples/groups.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "groups": { 4 | "$gid": { 5 | ".validate": "(newData.hasChildren() && members != null)", 6 | "members": { 7 | "$key1": { 8 | ".validate": "(((newData.hasChildren(['uid', 'name']) && $key1.length == 1) && $key1 >= '0') && $key1 <= '9')", 9 | "uid": { 10 | ".validate": "newData.isString()" 11 | }, 12 | "name": { 13 | ".validate": "newData.isString()" 14 | }, 15 | "$other": { 16 | ".validate": "false" 17 | } 18 | }, 19 | ".validate": "newData.hasChildren()" 20 | }, 21 | "$other": { 22 | ".validate": "false" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/issue-111.bolt: -------------------------------------------------------------------------------- 1 | type MyType { 2 | num: Number; 3 | str: String; 4 | any: Any; 5 | } 6 | 7 | path /path is MyType { 8 | read() { true } 9 | write() { true } 10 | } 11 | -------------------------------------------------------------------------------- /samples/issue-111.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "path": { 4 | ".validate": "newData.hasChildren(['num', 'str', 'any'])", 5 | "num": { 6 | ".validate": "newData.isNumber()" 7 | }, 8 | "str": { 9 | ".validate": "newData.isString()" 10 | }, 11 | "any": { 12 | ".validate": "true" 13 | }, 14 | "$other": { 15 | ".validate": "false" 16 | }, 17 | ".read": "true", 18 | ".write": "true" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/issue-118.bolt: -------------------------------------------------------------------------------- 1 | type Something { 2 | scalar: Number; 3 | scalarOrNull: Number | Null; 4 | array: Number[]; 5 | arrayOrNull: Number[] | Null; 6 | arrayOrScalar: Number[] | Number; 7 | 8 | map: Map; 9 | mapOrScalar: Map | Number; 10 | } 11 | 12 | path /path is Something { 13 | read() { true } 14 | write() { true } 15 | } 16 | -------------------------------------------------------------------------------- /samples/issue-118.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "path": { 4 | ".validate": "newData.hasChildren(['scalar'])", 5 | "scalar": { 6 | ".validate": "newData.isNumber()" 7 | }, 8 | "scalarOrNull": { 9 | ".validate": "newData.isNumber()" 10 | }, 11 | "array": { 12 | "$key1": { 13 | ".validate": "newData.isNumber()" 14 | }, 15 | ".validate": "newData.hasChildren()" 16 | }, 17 | "arrayOrNull": { 18 | "$key1": { 19 | ".validate": "newData.isNumber()" 20 | }, 21 | ".validate": "newData.hasChildren()" 22 | }, 23 | "arrayOrScalar": { 24 | "$key1": { 25 | ".validate": "newData.isNumber()" 26 | }, 27 | ".validate": "(newData.hasChildren() || newData.isNumber())" 28 | }, 29 | "map": { 30 | "$key1": { 31 | ".validate": "newData.isNumber()" 32 | }, 33 | ".validate": "newData.hasChildren()" 34 | }, 35 | "mapOrScalar": { 36 | "$key1": { 37 | ".validate": "newData.isNumber()" 38 | }, 39 | ".validate": "(newData.hasChildren() || newData.isNumber())" 40 | }, 41 | "$other": { 42 | ".validate": "false" 43 | }, 44 | ".read": "true", 45 | ".write": "true" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/issue-136.bolt: -------------------------------------------------------------------------------- 1 | type Entity { 2 | entityType: String 3 | } 4 | 5 | path /entity/{entity} is Entity { 6 | validate(){ 7 | "project|user".includes(this.entityType) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/issue-136.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "entity": { 4 | "$entity": { 5 | ".validate": "(newData.hasChildren(['entityType']) && 'project|user'.contains(newData.child('entityType').val()))", 6 | "entityType": { 7 | ".validate": "newData.isString()" 8 | }, 9 | "$other": { 10 | ".validate": "false" 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/issue-169.bolt: -------------------------------------------------------------------------------- 1 | path /test is Map {} 2 | 3 | type Test extends String { 4 | validate() { prior(root) != null } 5 | } 6 | -------------------------------------------------------------------------------- /samples/issue-169.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "test": { 4 | "$key1": { 5 | ".validate": "(root.val() != null && newData.isString())" 6 | }, 7 | ".validate": "newData.hasChildren()" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/issue-232.bolt: -------------------------------------------------------------------------------- 1 | function isAuth() { 2 | (auth != null); 3 | } 4 | 5 | function isUser(user) { 6 | isAuth() && (user == auth.uid); 7 | } 8 | 9 | function isAdmin() { 10 | isAuth() && (root.child('admins').child(auth.uid).val() != null); 11 | } 12 | 13 | path /users/{key1} { 14 | read() { 15 | isUser(key1) || isAdmin(); 16 | } 17 | } 18 | 19 | path /profile/{key1} { 20 | create() { 21 | isUser(key1); 22 | } 23 | 24 | update() { 25 | isUser(key1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/issue-232.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "users": { 4 | "$key1": { 5 | ".read": "((auth != null && $key1 == auth.uid) || (auth != null && root.child('admins').child(auth.uid).val() != null))" 6 | } 7 | }, 8 | "profile": { 9 | "$key1": { 10 | ".write": "((data.val() == null && (auth != null && $key1 == auth.uid)) || ((data.val() != null && newData.val() != null) && (auth != null && $key1 == auth.uid)))" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/issue-97.bolt: -------------------------------------------------------------------------------- 1 | path /parent { 2 | read() { auth !== null } 3 | 4 | /someKey { 5 | write() { false } 6 | } 7 | 8 | /{all_other_keys} { 9 | write() { auth !== null } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/issue-97.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "parent": { 4 | "someKey": { 5 | ".validate": "true", 6 | ".write": "false" 7 | }, 8 | "$all_other_keys": { 9 | ".write": "auth != null" 10 | }, 11 | ".read": "auth != null" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/mail.bolt: -------------------------------------------------------------------------------- 1 | // Mail.bolt - Inbox and Outbox Rules. 2 | // 3 | // These rules support putting Messages in an Inbox (for another user) 4 | // and making a record of all email sent in an outbox. 5 | // 6 | // Design goals: 7 | // 8 | // - Anyone can send a message to anybody by posting in the users inbox, as long as they have 9 | // the correct from field. 10 | // - Cannot forge the from field of a message. 11 | // - Cannot alter a message once created (it can only be deleted in-toto). 12 | // 13 | // Functionally similar rules to the corresponding rules defined in the Blaze Compiler: 14 | // https://github.com/firebase/blaze_compiler/blob/master/examples/mail_example.yaml 15 | 16 | path /users/{userid} { 17 | // Inbox 18 | /inbox/{msg} is Message { 19 | read() { isCurrentUser(userid) } 20 | write() { isNew(this) || isCurrentUser(userid) } 21 | } 22 | 23 | // Outbox 24 | /outbox/{msg} is Message { 25 | read() { isCurrentUser(userid) } 26 | write() { isCurrentUser(userid) } 27 | } 28 | } 29 | 30 | type Message { 31 | validate() { isNew(this) && isCurrentUser(this.from) } 32 | 33 | from: UserID, 34 | to: UserID, 35 | message: String, 36 | } 37 | 38 | type UserID extends String; 39 | 40 | isNew(ref) { prior(ref) == null } 41 | 42 | isCurrentUser(userid) { auth != null && auth.uid == userid } 43 | -------------------------------------------------------------------------------- /samples/mail.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "users": { 4 | "$userid": { 5 | "inbox": { 6 | "$msg": { 7 | ".validate": "((newData.hasChildren(['from', 'to', 'message']) && data.val() == null) && (auth != null && auth.uid == newData.child('from').val()))", 8 | "from": { 9 | ".validate": "newData.isString()" 10 | }, 11 | "to": { 12 | ".validate": "newData.isString()" 13 | }, 14 | "message": { 15 | ".validate": "newData.isString()" 16 | }, 17 | "$other": { 18 | ".validate": "false" 19 | }, 20 | ".read": "(auth != null && auth.uid == $userid)", 21 | ".write": "(data.val() == null || (auth != null && auth.uid == $userid))" 22 | } 23 | }, 24 | "outbox": { 25 | "$msg": { 26 | ".validate": "((newData.hasChildren(['from', 'to', 'message']) && data.val() == null) && (auth != null && auth.uid == newData.child('from').val()))", 27 | "from": { 28 | ".validate": "newData.isString()" 29 | }, 30 | "to": { 31 | ".validate": "newData.isString()" 32 | }, 33 | "message": { 34 | ".validate": "newData.isString()" 35 | }, 36 | "$other": { 37 | ".validate": "false" 38 | }, 39 | ".read": "(auth != null && auth.uid == $userid)", 40 | ".write": "(auth != null && auth.uid == $userid)" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/map-scalar.bolt: -------------------------------------------------------------------------------- 1 | path / is Test; 2 | 3 | type Test { 4 | s1: ShortString, 5 | s2: AliasString, 6 | m1: Map, 7 | m2: Map 8 | } 9 | 10 | type AliasString extends ShortString; 11 | type ShortString extends String { 12 | validate() { this.length >= 0 && this.length <= 16 } 13 | } 14 | -------------------------------------------------------------------------------- /samples/map-scalar.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".validate": "newData.hasChildren(['s1', 's2'])", 4 | "s1": { 5 | ".validate": "((newData.isString() && newData.val().length >= 0) && newData.val().length <= 16)" 6 | }, 7 | "s2": { 8 | ".validate": "((newData.isString() && newData.val().length >= 0) && newData.val().length <= 16)" 9 | }, 10 | "m1": { 11 | "$key1": { 12 | ".validate": "(($key1.length >= 0 && $key1.length <= 16) && newData.isNumber())" 13 | }, 14 | ".validate": "newData.hasChildren()" 15 | }, 16 | "m2": { 17 | "$key2": { 18 | ".validate": "(($key2.length >= 0 && $key2.length <= 16) && newData.isNumber())" 19 | }, 20 | ".validate": "newData.hasChildren()" 21 | }, 22 | "$other": { 23 | ".validate": "false" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/multi-update.bolt: -------------------------------------------------------------------------------- 1 | // Firebase can update multiple paths with a single update. 2 | // When doing so - you can write a rule that ensures 3 | // that multiple paths are atomically updated. 4 | // 5 | // This example ensures that a user and his secret (string) 6 | // are only updated when the other one exists. 7 | 8 | path /secret/{uid} is String { 9 | read() { isUser(uid) } 10 | validate() { root.users[uid] != null } 11 | } 12 | 13 | path /users/{uid} is User { 14 | read() { true } 15 | validate() { root.secret[uid] != null } 16 | } 17 | 18 | type User { 19 | name: String 20 | } 21 | 22 | isUser(uid) { auth != null && auth.uid == uid } 23 | -------------------------------------------------------------------------------- /samples/multi-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "secret": { 4 | "$uid": { 5 | ".validate": "(newData.isString() && newData.parent().parent().child('users').child($uid).val() != null)", 6 | ".read": "(auth != null && auth.uid == $uid)" 7 | } 8 | }, 9 | "users": { 10 | "$uid": { 11 | ".validate": "(newData.hasChildren(['name']) && newData.parent().parent().child('secret').child($uid).val() != null)", 12 | "name": { 13 | ".validate": "newData.isString()" 14 | }, 15 | "$other": { 16 | ".validate": "false" 17 | }, 18 | ".read": "true" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /samples/regexp.bolt: -------------------------------------------------------------------------------- 1 | path / { 2 | read() { true } 3 | write() { true } 4 | } 5 | 6 | path /ss is SocialSecurity; 7 | path /integer is IntegerString; 8 | path /float is FloatString; 9 | path /int is Integer; 10 | path /alpha is Alpha; 11 | path /year is Year; 12 | path /date is ISODate; 13 | path /slug is Slug; 14 | path /domain is Domain; 15 | 16 | type SocialSecurity extends String { 17 | validate() { this.test(/^\d\d\d-\d\d-\d\d\d\d$/) } 18 | } 19 | 20 | // Only an integer string 21 | type IntegerString extends String { 22 | validate() { this.test(/^-?\d+$/) } 23 | } 24 | 25 | // A floating point number (or integer). 26 | type FloatString extends String { 27 | validate() { this.test(/^-?(\d+\.?\d*|\.\d+)$/) } 28 | } 29 | 30 | // An integral number (no fractional part) 31 | type Integer extends Number { 32 | validate() { (this + '').test(/^-?\d+$/) } 33 | } 34 | 35 | // Only letters 36 | type Alpha extends String { 37 | validate() { this.test(/^[a-z]+$/i) } 38 | } 39 | 40 | // YYYY is 20th or 21st century 41 | type Year extends String { 42 | validate() { this.test(/^(19|20)\d\d$/) } 43 | } 44 | 45 | // YYYY-MM-DD in 20th or 21st century 46 | // Note: Does not validate day-of-month is valid. 47 | type ISODate extends String { 48 | validate() { this.test(/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/) } 49 | } 50 | 51 | type Slug extends String { 52 | validate() { this.test(/^([a-z0-9]+-)+[a-z0-9]+$/) } 53 | } 54 | 55 | type Domain extends String { 56 | validate() { this.test(/^.+\.(com|org|edu)$/) } 57 | } 58 | -------------------------------------------------------------------------------- /samples/regexp.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "true", 4 | ".write": "true", 5 | "ss": { 6 | ".validate": "(newData.isString() && newData.val().matches(/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/))" 7 | }, 8 | "integer": { 9 | ".validate": "(newData.isString() && newData.val().matches(/^-?\\d+$/))" 10 | }, 11 | "float": { 12 | ".validate": "(newData.isString() && newData.val().matches(/^-?(\\d+\\.?\\d*|\\.\\d+)$/))" 13 | }, 14 | "int": { 15 | ".validate": "(newData.isNumber() && (newData.val() + '').matches(/^-?\\d+$/))" 16 | }, 17 | "alpha": { 18 | ".validate": "(newData.isString() && newData.val().matches(/^[a-z]+$/i))" 19 | }, 20 | "year": { 21 | ".validate": "(newData.isString() && newData.val().matches(/^(19|20)\\d\\d$/))" 22 | }, 23 | "date": { 24 | ".validate": "(newData.isString() && newData.val().matches(/^(19|20)\\d\\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/))" 25 | }, 26 | "slug": { 27 | ".validate": "(newData.isString() && newData.val().matches(/^([a-z0-9]+-)+[a-z0-9]+$/))" 28 | }, 29 | "domain": { 30 | ".validate": "(newData.isString() && newData.val().matches(/^.+\\.(com|org|edu)$/))" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/serialized.bolt: -------------------------------------------------------------------------------- 1 | /* 2 | * Demonstrate using a generic serialized update type. 3 | * Updates can only be made if the counter property 4 | * is created with value 1, and each update must 5 | * increment the value by 1 (conflicting, non-serialized updates 6 | * will fail). 7 | */ 8 | path /products is Map> { 9 | read() { true } 10 | } 11 | 12 | type Product { 13 | name: String, 14 | cost: Number, 15 | } 16 | 17 | type Serialized extends T { 18 | counter: Counter, 19 | } 20 | 21 | type Counter extends Number { 22 | validate() { (prior(this) == null && this == 1) || this == prior(this) + 1 } 23 | } 24 | -------------------------------------------------------------------------------- /samples/serialized.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "products": { 4 | "$key1": { 5 | ".validate": "newData.hasChildren(['name', 'cost', 'counter'])", 6 | "name": { 7 | ".validate": "newData.isString()" 8 | }, 9 | "cost": { 10 | ".validate": "newData.isNumber()" 11 | }, 12 | "$other": { 13 | ".validate": "false" 14 | }, 15 | "counter": { 16 | ".validate": "(newData.isNumber() && ((data.val() == null && newData.val() == 1) || newData.val() == data.val() + 1))" 17 | } 18 | }, 19 | ".validate": "newData.hasChildren()", 20 | ".read": "true" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/type-extension.bolt: -------------------------------------------------------------------------------- 1 | type PositiveInteger extends Number { 2 | validate() { return this >= 0; } 3 | } 4 | 5 | type UnixTimestamp extends PositiveInteger {} 6 | 7 | type NonEmptyString extends String { 8 | validate() { return this.length > 0; } 9 | } 10 | 11 | type URL extends String { 12 | validate() { return this.startsWith('http'); } 13 | } 14 | 15 | type Test { 16 | time: UnixTimestamp 17 | name: NonEmptyString 18 | url: URL 19 | } 20 | 21 | 22 | path / is Test {} 23 | -------------------------------------------------------------------------------- /samples/type-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".validate": "newData.hasChildren(['time', 'name', 'url'])", 4 | "time": { 5 | ".validate": "(newData.isNumber() && newData.val() >= 0)" 6 | }, 7 | "name": { 8 | ".validate": "(newData.isString() && newData.val().length > 0)" 9 | }, 10 | "url": { 11 | ".validate": "(newData.isString() && newData.val().beginsWith('http'))" 12 | }, 13 | "$other": { 14 | ".validate": "false" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/user-security.bolt: -------------------------------------------------------------------------------- 1 | // user-security.bolt - Chat room example. 2 | // 3 | // Design goals: 4 | // 5 | // - Any logged in user can get a list of room names. 6 | // - Room names are read-only 7 | // - Users can only add/remove themselves to rooms. 8 | // - Users can go by a different nickname in each room. 9 | // - Messages can be read by any room member. 10 | // - Messages can be created by any member. 11 | // - Messages cannot be modified or deleted. 12 | // - Messages are signed by user's uid, and timestamped. 13 | // 14 | // See https://www.firebase.com/docs/security/guide/user-security.html 15 | 16 | path /room_names { 17 | read() { auth != null } 18 | 19 | /{room_id} is String; 20 | } 21 | 22 | path /members/{room_id} { 23 | read() { this[auth.uid] != null } 24 | 25 | /{user_id} is Nickname { 26 | write() { isCurrentUser(user_id) } 27 | } 28 | } 29 | 30 | path /messages/{room_id} { 31 | read() { root.room_names[room_id] != null } 32 | 33 | /{message_id} is Message { 34 | write() { root.members[room_id][auth.uid] != null } 35 | } 36 | } 37 | 38 | type Nickname extends String { 39 | validate() { this.length > 0 && this.length < 20 } 40 | } 41 | 42 | type UserID extends String { 43 | validate() { isCurrentUser(this) } 44 | } 45 | 46 | type MessageString extends String { 47 | validate() { this.length > 0 && this.length < 50 } 48 | } 49 | 50 | type Timestamp extends Number { 51 | validate() { this <= now } 52 | } 53 | 54 | type Message { 55 | validate() { isNew(this) } 56 | 57 | user: UserID, 58 | message: MessageString, 59 | timestamp: Timestamp 60 | } 61 | 62 | isCurrentUser(id) { auth != null && auth.uid == id } 63 | 64 | isNew(ref) { prior(ref) == null } 65 | -------------------------------------------------------------------------------- /samples/user-security.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "room_names": { 4 | ".read": "auth != null", 5 | "$room_id": { 6 | ".validate": "newData.isString()" 7 | } 8 | }, 9 | "members": { 10 | "$room_id": { 11 | ".read": "data.child(auth.uid).val() != null", 12 | "$user_id": { 13 | ".validate": "((newData.isString() && newData.val().length > 0) && newData.val().length < 20)", 14 | ".write": "(auth != null && auth.uid == $user_id)" 15 | } 16 | } 17 | }, 18 | "messages": { 19 | "$room_id": { 20 | ".read": "root.child('room_names').child($room_id).val() != null", 21 | "$message_id": { 22 | ".validate": "(newData.hasChildren(['user', 'message', 'timestamp']) && data.val() == null)", 23 | "user": { 24 | ".validate": "(newData.isString() && (auth != null && auth.uid == newData.val()))" 25 | }, 26 | "message": { 27 | ".validate": "((newData.isString() && newData.val().length > 0) && newData.val().length < 50)" 28 | }, 29 | "timestamp": { 30 | ".validate": "(newData.isNumber() && newData.val() <= now)" 31 | }, 32 | "$other": { 33 | ".validate": "false" 34 | }, 35 | ".write": "newData.parent().parent().parent().child('members').child($room_id).child(auth.uid).val() != null" 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/userdoc.bolt: -------------------------------------------------------------------------------- 1 | // Rules defined using new Rules Engine syntax. 2 | 3 | type Document extends Object {} 4 | 5 | type Date extends String { 6 | // validate() { return isISODate(this); } 7 | } 8 | 9 | type Userid extends String { 10 | // validate() { return userExists(this); } 11 | } 12 | 13 | type Docid extends String {} 14 | 15 | type Metadata { 16 | created: Date, 17 | modified: Date, 18 | title: String, 19 | 20 | // extensible(prop) { return true; } 21 | } 22 | 23 | type Permission { 24 | owner: Userid, 25 | grantee: Userid, 26 | docid: Docid 27 | } 28 | 29 | path /documents/{ownerid} { 30 | read() { return isCurrentUser(ownerid); } 31 | write() { return isCurrentUser(ownerid); } 32 | 33 | /{docid} is Document { 34 | read() { return hasPermission(ownerid, docid, 'read'); } 35 | write() { return hasPermission(ownerid, docid, 'write'); } 36 | } 37 | } 38 | 39 | // Allow reading the list of all a user's own metadata. 40 | path /metadata/{ownerid} { 41 | read() { return isCurrentUser(ownerid); } 42 | write() { return isCurrentUser(ownerid); } 43 | 44 | /{docid} is Metadata { 45 | read() { return hasPermission(ownerid, docid, 'read'); } 46 | write() { return hasPermission(ownerid, docid, 'write'); } 47 | } 48 | } 49 | 50 | path /permissions { 51 | read() { return isLoggedIn(); } 52 | index() { return ['owner', 'grantee', 'docid']; } 53 | 54 | /{key} is Permission { 55 | write() { 56 | return this == null && isCurrentUser(prior(this.owner)) || 57 | this != null && 58 | // Limit structure of the key format to "grantee|owner|docid" 59 | key == fullId(this.grantee, this.owner, this.docid) && 60 | isCurrentUser(this.owner) && 61 | docIsOwnedBy(this.docid, currentUser()); 62 | } 63 | // No read() rule neeeded since this is only used in hasPermission() 64 | } 65 | } 66 | 67 | isLoggedIn() { auth != null } 68 | isCurrentUser(userid) { currentUser() == userid } 69 | currentUser() { auth.uid } 70 | fullId(userid, ownerid, docid) { userid + '|' + ownerid + '|' + docid } 71 | hasPermission(ownerid, docid, p) { root.permissions[fullId(currentUser(), ownerid, docid)][p] } 72 | docIsOwnedBy(docid, userid) { root.documents[userid][docid] != null } 73 | userExists(userid) { root.users[userid] != null } 74 | 75 | // TODO: Use .match instead - needs specific translator for string->regexp in language. 76 | isISODate(date) { beginsWith(date, '2015-') } 77 | -------------------------------------------------------------------------------- /samples/userdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "documents": { 4 | "$ownerid": { 5 | ".read": "auth.uid == $ownerid", 6 | ".write": "auth.uid == $ownerid", 7 | "$docid": { 8 | ".validate": "newData.hasChildren()", 9 | ".read": "root.child('permissions').child(auth.uid + '|' + $ownerid + '|' + $docid).child('read').val() == true", 10 | ".write": "newData.parent().parent().parent().child('permissions').child(auth.uid + '|' + $ownerid + '|' + $docid).child('write').val() == true" 11 | } 12 | } 13 | }, 14 | "metadata": { 15 | "$ownerid": { 16 | ".read": "auth.uid == $ownerid", 17 | ".write": "auth.uid == $ownerid", 18 | "$docid": { 19 | ".validate": "newData.hasChildren(['created', 'modified', 'title'])", 20 | "created": { 21 | ".validate": "newData.isString()" 22 | }, 23 | "modified": { 24 | ".validate": "newData.isString()" 25 | }, 26 | "title": { 27 | ".validate": "newData.isString()" 28 | }, 29 | "$other": { 30 | ".validate": "false" 31 | }, 32 | ".read": "root.child('permissions').child(auth.uid + '|' + $ownerid + '|' + $docid).child('read').val() == true", 33 | ".write": "newData.parent().parent().parent().child('permissions').child(auth.uid + '|' + $ownerid + '|' + $docid).child('write').val() == true" 34 | } 35 | } 36 | }, 37 | "permissions": { 38 | ".read": "auth != null", 39 | ".indexOn": [ 40 | "owner", 41 | "grantee", 42 | "docid" 43 | ], 44 | "$key": { 45 | ".validate": "newData.hasChildren(['owner', 'grantee', 'docid'])", 46 | "owner": { 47 | ".validate": "newData.isString()" 48 | }, 49 | "grantee": { 50 | ".validate": "newData.isString()" 51 | }, 52 | "docid": { 53 | ".validate": "newData.isString()" 54 | }, 55 | "$other": { 56 | ".validate": "false" 57 | }, 58 | ".write": "((newData.val() == null && auth.uid == data.child('owner').val()) || (((newData.val() != null && $key == newData.child('grantee').val() + '|' + newData.child('owner').val() + '|' + newData.child('docid').val()) && auth.uid == newData.child('owner').val()) && newData.parent().parent().child('documents').child(auth.uid).child(newData.child('docid').val()).val() != null))" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/bolt.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // TODO(koss): After node 0.10 leaves LTS - remove polyfilled Promise library. 18 | if (typeof Promise === 'undefined') { 19 | require('es6-promise').polyfill(); 20 | } 21 | 22 | let parser = require('./rules-parser'); 23 | import * as generator from './rules-generator'; 24 | import * as astImport from './ast'; 25 | 26 | export let FILE_EXTENSION = 'bolt'; 27 | 28 | export let ast = astImport; 29 | export let parse = parser.parse; 30 | export let Generator = generator.Generator; 31 | export let decodeExpression = ast.decodeExpression; 32 | export let generate = generator.generate; 33 | -------------------------------------------------------------------------------- /src/file-io.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as fs from 'fs'; 17 | import * as util from './util'; 18 | 19 | const hasXMLHttpRequest = 20 | typeof global !== 'undefined' && ( global)['XMLHttpRequest'] !== undefined; 21 | 22 | export interface ReadFileResult { 23 | content: string; 24 | url: string; 25 | } 26 | 27 | export function readJSONFile(path: string, fnFallback?: () => any): Promise { 28 | return readFile(path) 29 | .then(function(response: ReadFileResult) { 30 | return JSON.parse(response.content); 31 | }) 32 | .catch(function(error) { 33 | if (error.code === 'ENOENT' && typeof fnFallback === 'function') { 34 | return fnFallback(); 35 | } 36 | throw error; 37 | }); 38 | } 39 | 40 | export function writeJSONFile(path: string, data: any): Promise { 41 | return writeFile(path, util.prettyJSON(data)); 42 | } 43 | 44 | export function readFile(path: string): Promise { 45 | return hasXMLHttpRequest ? request('GET', path) : readFS(path); 46 | } 47 | 48 | export function writeFile(path: string, data: any): Promise { 49 | return hasXMLHttpRequest ? request('PUT', path, data) : writeFS(path, data); 50 | } 51 | 52 | function request(method: string, url: string, data?: any): Promise { 53 | return new Promise(function(resolve, reject) { 54 | let req = new XMLHttpRequest(); 55 | 56 | req.open(method, '/' + url); 57 | 58 | req.onload = function() { 59 | if (req.status === 200) { 60 | resolve({content: req.response, url: url}); 61 | } else { 62 | reject(new Error(url + " " + req.statusText)); 63 | } 64 | }; 65 | 66 | req.onerror = function() { 67 | reject(new Error(url + " Network Error")); 68 | }; 69 | 70 | if (data) { 71 | req.setRequestHeader('Content-Type', 'text'); 72 | } 73 | 74 | req.send(data); 75 | }); 76 | } 77 | 78 | function readFS(path: string): Promise { 79 | return new Promise(function(resolve, reject) { 80 | fs.readFile(path, {encoding: 'utf8'}, function(error, data) { 81 | if (error) { 82 | reject(error); 83 | return; 84 | } 85 | resolve({url: path, content: data}); 86 | }); 87 | }); 88 | } 89 | 90 | function writeFS(path: string, data: any): Promise { 91 | return new Promise(function(resolve, reject) { 92 | fs.writeFile(path, data, {encoding: 'utf8'}, function(error) { 93 | if (error) { 94 | reject(error); 95 | return; 96 | } 97 | resolve({url: path, content: "ok"}); 98 | }); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /src/firebase-rest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase helper functions for REST API (using Promises). 3 | * 4 | * Copyright 2015 Google Inc. All Rights Reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | import https = require('https'); 19 | import http = require('http'); 20 | import * as util from './util'; 21 | import querystring = require('querystring'); 22 | let uuid = require('node-uuid'); 23 | var FirebaseTokenGenerator = require('firebase-token-generator'); 24 | 25 | var FIREBASE_HOST = 'firebaseio.com'; 26 | var DEBUG_HEADER = 'x-firebase-auth-debug'; 27 | 28 | export var RULES_LOCATION = '/.settings/rules'; 29 | export var TIMESTAMP = { 30 | '.sv': 'timestamp' 31 | }; 32 | 33 | export type RequestOptions = { 34 | method: string, 35 | print?: string; 36 | } 37 | 38 | export class Client { 39 | private debug = false; 40 | 41 | constructor( 42 | private appName: string, private authToken?: string, 43 | public uid?: string) {} 44 | 45 | setDebug(debug = true) { 46 | this.debug = debug; 47 | return this; 48 | } 49 | 50 | get(location: string): Promise { 51 | return this.request({method: 'GET'}, location); 52 | } 53 | 54 | put(location: string, content: any): Promise { 55 | return this.request({method: 'PUT', print: 'silent'}, location, content); 56 | } 57 | 58 | request(opt: RequestOptions, path: string, content?: any) { 59 | var options = { 60 | hostname: this.appName + '.' + FIREBASE_HOST, 61 | path: path + '.json', 62 | method: opt.method, 63 | }; 64 | 65 | var query: any = {}; 66 | if (opt.print) { 67 | query.print = opt.print; 68 | } 69 | 70 | if (this.authToken) { 71 | query.auth = this.authToken; 72 | } 73 | 74 | if (Object.keys(query).length > 0) { 75 | options.path += '?' + querystring.stringify(query); 76 | } 77 | 78 | content = util.prettyJSON(content); 79 | 80 | return request(options, content, this.debug).then(function(body: string) { 81 | return body === '' ? null : JSON.parse(body); 82 | }); 83 | } 84 | } 85 | 86 | var ridNext = 0; 87 | 88 | function request(options: any, content: any, debug: boolean): Promise { 89 | ridNext += 1; 90 | var rid = ridNext; 91 | 92 | function log(s: string): void { 93 | if (debug) { 94 | console.error('Request<' + rid + '>: ' + s); 95 | } 96 | } 97 | 98 | log('Request: ' + util.prettyJSON(options)); 99 | if (content) { 100 | log('Body: \'' + content + '\''); 101 | } 102 | 103 | return new Promise(function(resolve, reject) { 104 | // TODO: Why isn't this argument typed as per https.request? 105 | var req = https.request(options, function(res: http.ClientResponse) { 106 | var chunks = []; 107 | 108 | res.on('data', function(body: string) { 109 | chunks.push(body); 110 | }); 111 | 112 | res.on('end', function() { 113 | var result: string = chunks.join(''); 114 | log('Result (' + res.statusCode + '): \'' + result + '\''); 115 | let message = 'Status = ' + res.statusCode + ' ' + result; 116 | 117 | // Dump debug information if present for both successful and failed 118 | // requests. 119 | if (res.headers[DEBUG_HEADER]) { 120 | let formattedHeader = 121 | res.headers[DEBUG_HEADER].split(' /').join('\n /'); 122 | log(formattedHeader); 123 | message += '\n' + formattedHeader; 124 | } 125 | 126 | if (res.statusCode && Math.floor(res.statusCode / 100) !== 2) { 127 | reject(new Error(message)); 128 | } else { 129 | resolve(result); 130 | } 131 | }); 132 | }); 133 | 134 | if (content) { 135 | req.write(content, 'utf8'); 136 | } 137 | req.end(); 138 | 139 | req.on('error', function(error: Error) { 140 | log('Request error: ' + error); 141 | reject(error); 142 | }); 143 | }); 144 | } 145 | 146 | // opts { debug: Boolean, admin: Boolean } 147 | export function generateUidAuthToken(secret: string, opts: any) { 148 | opts = util.extend({debug: false, admin: false}, opts); 149 | 150 | var tokenGenerator = new FirebaseTokenGenerator(secret); 151 | var uid = uuid.v4(); 152 | var token = tokenGenerator.createToken({uid: uid}, opts); 153 | return {uid: uid, token: token}; 154 | } 155 | 156 | /** 157 | * Fancy ID generator that creates 20-character string identifiers with the 158 | * following properties: 159 | * 160 | * 1. They're based on timestamp so that they sort *after* any existing ids. 161 | * 2. They contain 72-bits of random data after the timestamp so that IDs won't 162 | * collide with other clients' IDs. 163 | * 3. They sort *lexicographically* (so the timestamp is converted to characters 164 | * that will sort properly). 165 | * 4. They're monotonically increasing. Even if you generate more than one in 166 | * the same timestamp, the latter ones will sort after the former ones. We do 167 | * this by using the previous random bits but "incrementing" them by 1 (only in 168 | * the case of a timestamp collision). 169 | */ 170 | 171 | // Modeled after base64 web-safe chars, but ordered by ASCII. 172 | var PUSH_CHARS = 173 | '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; 174 | 175 | // Timestamp of last push, used to prevent local collisions if you push twice in 176 | // one ms. 177 | var lastPushTime = 0; 178 | 179 | // We generate 72-bits of randomness which get turned into 12 characters and 180 | // appended to the timestamp to prevent collisions with other clients. We store 181 | // the last characters we generated because in the event of a collision, we'll 182 | // use those same characters except "incremented" by one. 183 | var lastRandChars = []; 184 | 185 | export function generatePushID(): string { 186 | var now = new Date().getTime(); 187 | var duplicateTime = (now === lastPushTime); 188 | lastPushTime = now; 189 | 190 | var timeStampChars = new Array(8); 191 | for (var i = 7; i >= 0; i--) { 192 | timeStampChars[i] = PUSH_CHARS.charAt(now % 64); 193 | // NOTE: Can't use << here because javascript will convert to int and lose 194 | // the upper bits. 195 | now = Math.floor(now / 64); 196 | } 197 | if (now !== 0) { 198 | throw new Error('We should have converted the entire timestamp.'); 199 | } 200 | 201 | var id = timeStampChars.join(''); 202 | 203 | if (!duplicateTime) { 204 | for (i = 0; i < 12; i++) { 205 | lastRandChars[i] = Math.floor(Math.random() * 64); 206 | } 207 | } else { 208 | // If the timestamp hasn't changed since last push, use the same random 209 | // number, except incremented by 1. 210 | for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) { 211 | lastRandChars[i] = 0; 212 | } 213 | lastRandChars[i]++; 214 | } 215 | for (i = 0; i < 12; i++) { 216 | id += PUSH_CHARS.charAt(lastRandChars[i]); 217 | } 218 | if (id.length !== 20) { 219 | throw new Error('Length should be 20.'); 220 | } 221 | 222 | return id; 223 | } 224 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | let lastError: string | undefined; 17 | let lastMessage: string | undefined; 18 | let errorCount: number; 19 | let silenceOutput: boolean; 20 | 21 | let DEBUG = false; 22 | 23 | let getContext = () => ( {}); 24 | 25 | reset(); 26 | 27 | export function reset() { 28 | lastError = undefined; 29 | lastMessage = undefined; 30 | errorCount = 0; 31 | silenceOutput = false; 32 | } 33 | 34 | export function setDebug(debug = true) { 35 | DEBUG = debug; 36 | } 37 | 38 | export function silent(f = true) { 39 | silenceOutput = f; 40 | } 41 | 42 | export interface ErrorContext { 43 | line?: number; 44 | column?: number; 45 | } 46 | 47 | export function setContext(fn: () => ErrorContext) { 48 | getContext = fn; 49 | } 50 | 51 | export function error(s: string) { 52 | let err = errorString(s); 53 | // De-dup identical messages 54 | if (err === lastMessage) { 55 | return; 56 | } 57 | lastMessage = err; 58 | lastError = lastMessage; 59 | if (!silenceOutput) { 60 | console.error(lastError); 61 | if (DEBUG) { 62 | let e = new Error("Stack trace"); 63 | console.error(e.stack); 64 | } 65 | } 66 | errorCount += 1; 67 | } 68 | 69 | export function warn(s: string) { 70 | let err = errorString(s); 71 | // De-dup identical messages 72 | if (err === lastMessage) { 73 | return; 74 | } 75 | lastMessage = err; 76 | if (!silenceOutput) { 77 | console.warn(lastMessage); 78 | } 79 | } 80 | 81 | export function getLastMessage(): string | undefined { 82 | return lastMessage; 83 | } 84 | 85 | function errorString(s: string) { 86 | let ctx = getContext(); 87 | if (ctx.line !== undefined && ctx.column !== undefined) { 88 | return 'bolt:' + ctx.line + ':' + ctx.column + ': ' + s; 89 | } else { 90 | return 'bolt: ' + s; 91 | } 92 | } 93 | 94 | export function hasErrors(): boolean { 95 | return errorCount > 0; 96 | } 97 | 98 | export function errorSummary(): string { 99 | if (errorCount === 1) { 100 | return lastError; 101 | } 102 | 103 | if (errorCount !== 0) { 104 | return "Fatal errors: " + errorCount; 105 | } 106 | return ""; 107 | } 108 | -------------------------------------------------------------------------------- /src/parse-util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | let parser = require('./rules-parser'); 17 | import * as ast from './ast'; 18 | 19 | export function parseExpression(expression: string): ast.Exp { 20 | var result = parser.parse('function f() {return ' + expression + ';}'); 21 | return result.functions.f.body; 22 | } 23 | -------------------------------------------------------------------------------- /src/simulator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase Rules test simulator. 3 | * 4 | * Copyright 2015 Google Inc. All Rights Reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | import {assert} from 'chai'; 19 | import * as rest from './firebase-rest'; 20 | import * as util from './util'; 21 | import * as fileIO from './file-io'; 22 | import * as bolt from './bolt'; 23 | 24 | let generate = util.lift(bolt.generate); 25 | let readFile = util.liftArgs(fileIO.readFile); 26 | 27 | var MAX_TEST_MS = 60000; 28 | 29 | export type SuiteFunction = (fn: TestFunction) => void; 30 | 31 | // Interface for 'test' function passed back to rulesSuite callback. 32 | // test is a function as well as a namespace for some static methods 33 | // and constants. 34 | export interface TestFunction { 35 | (name: string, fnTest: (rules: RulesTest) => void): void; 36 | rules: (path: string) => void; 37 | database: (appName: string, secret: string) => void; 38 | uid: (username: string) => string; 39 | TIMESTAMP: Object; 40 | }; 41 | 42 | export function rulesSuite(suiteName: string, fnSuite: SuiteFunction) { 43 | new RulesSuite(suiteName, fnSuite).run(); 44 | } 45 | 46 | export class RulesSuite { 47 | public debug = false; 48 | private users = <{[name: string]: rest.Client}>{}; 49 | private tests = []; 50 | private rulesPath: string; 51 | private rulesPathResolve: (path: string) => void; 52 | private databaseReady: () => void; 53 | private ready: Promise; 54 | private adminClient: rest.Client; 55 | private appName: string; 56 | private appSecret: string; 57 | 58 | constructor(public suiteName: string, 59 | private fnSuite: SuiteFunction) {} 60 | 61 | setDebug(debug = true) { 62 | this.debug = debug; 63 | return this; 64 | } 65 | 66 | run() { 67 | var self = this; 68 | 69 | // Run Mocha Test Suite - serialize with any other mocha test suites. 70 | suite("Firebase Rules Simulator: " + self.suiteName, function() { 71 | this.timeout(MAX_TEST_MS); 72 | suiteSetup(function() { 73 | var rulesPath = new Promise(function(resolve) { 74 | self.rulesPathResolve = resolve; 75 | }); 76 | 77 | var database = new Promise(function(resolve) { 78 | self.databaseReady = resolve; 79 | }); 80 | 81 | var rulesJSON = generate(util.getProp(readFile(rulesPath), 'content')); 82 | 83 | self.ready = Promise.all([rulesJSON, database]) 84 | .then(self.onRulesReady.bind(self)); 85 | 86 | // Execute initialization and get test definitions (in client code). 87 | self.fnSuite(self.getInterface()); 88 | 89 | return self.ready; 90 | }); 91 | 92 | test("Initialization.", function() { 93 | // pass 94 | }); 95 | 96 | test("Rules test.", function() { 97 | return self.runTests(); 98 | }); 99 | }); 100 | } 101 | 102 | getInterface() { 103 | var test = this.test.bind(this); 104 | test.rules = this.rules.bind(this); 105 | test.database = this.database.bind(this); 106 | test.uid = this.uid.bind(this); 107 | test.TIMESTAMP = rest.TIMESTAMP; 108 | return test; 109 | } 110 | 111 | // Called when rules are generated and test database is known. 112 | onRulesReady(prereq: [Object, any]) { 113 | let rulesJSON = prereq[0]; 114 | return this.adminClient.put(rest.RULES_LOCATION, rulesJSON); 115 | } 116 | 117 | runTests() { 118 | var p = Promise.resolve(); 119 | 120 | function next(prev: Promise, test: RulesTest): Promise { 121 | return prev.then(function() { 122 | return test.run(); 123 | }); 124 | } 125 | 126 | for (var i = 0; i < this.tests.length; i++) { 127 | p = next(p, this.tests[i]); 128 | } 129 | 130 | return p; 131 | } 132 | 133 | test(testName: string, fnTest: (rules: RulesTest) => void): void { 134 | this.tests.push(new RulesTest(testName, this, fnTest)); 135 | } 136 | 137 | rules(rulesPath: string): void { 138 | if (this.rulesPath) { 139 | throw new Error("Only expect a single call to the test.rules function."); 140 | } 141 | this.rulesPath = rulesPath; 142 | this.rulesPathResolve(util.ensureExtension(rulesPath, bolt.FILE_EXTENSION)); 143 | } 144 | 145 | database(appName: string, appSecret: string) { 146 | if (this.adminClient) { 147 | throw new Error("Only expect a single call to the test.database function."); 148 | } 149 | this.appName = appName; 150 | this.appSecret = appSecret; 151 | this.adminClient = this.ensureUser('admin'); 152 | this.databaseReady(); 153 | } 154 | 155 | uid(username: string): string | undefined { 156 | return this.ensureUser(username).uid; 157 | } 158 | 159 | ensureUser(username: string): rest.Client { 160 | if (!(username in this.users)) { 161 | if (username === 'anon') { 162 | this.users[username] = new rest.Client(this.appName); 163 | } else { 164 | let tokenInfo = rest.generateUidAuthToken( 165 | this.appSecret, 166 | { debug: true, 167 | admin: username === 'admin' }); 168 | this.users[username] = new rest.Client(this.appName, tokenInfo.token, tokenInfo.uid); 169 | } 170 | } 171 | 172 | return this.users[username]; 173 | } 174 | } 175 | 176 | interface Step { 177 | label: string; 178 | fn: () => Promise; 179 | } 180 | 181 | export class RulesTest { 182 | private lastError: string; 183 | private steps: Step[] = []; 184 | private failed = false; 185 | private path: string | undefined; 186 | private client: rest.Client; 187 | private status: boolean | undefined; 188 | 189 | constructor(private testName: string, 190 | private suite: RulesSuite, 191 | private fnTest: (rules: RulesTest) => void) {} 192 | 193 | run() { 194 | this.debug(false); 195 | this.as('admin'); 196 | this.at('/'); 197 | this.write(null); 198 | this.succeeds("initialization"); 199 | 200 | this.at(undefined); 201 | this.as('anon'); 202 | 203 | this.fnTest(this); 204 | 205 | this.debug(false); 206 | 207 | return this.executeQueue() 208 | .then(() => { 209 | this.log("Finished"); 210 | }) 211 | .catch((error) => { 212 | this.log("Failed: " + error); 213 | throw error; 214 | }); 215 | } 216 | 217 | // Queue a function to be called in sequence after previous step 218 | // in test is (successfully) completed. 219 | queue(op: string, args: ArrayLike, fn: () => Promise) { 220 | if (this.failed) { 221 | return; 222 | } 223 | let argsT = util.copyArray(args).map(function(x) { 224 | return util.prettyJSON(x); 225 | }); 226 | var label = op + '(' + argsT.join(', ') + ')'; 227 | this.steps.push({label: label, fn: fn}); 228 | } 229 | 230 | executeQueue() { 231 | var self = this; 232 | 233 | this.log("Executing (" + this.steps.length + " steps)"); 234 | var p = Promise.resolve(true); 235 | 236 | function next(prev: Promise, step: Step): Promise { 237 | return prev.then(function() { 238 | self.log(step.label); 239 | return step.fn(); 240 | }); 241 | } 242 | 243 | for (var i = 0; i < this.steps.length; i++) { 244 | p = next(p, this.steps[i]); 245 | } 246 | 247 | return p; 248 | } 249 | 250 | debug(debug?: boolean): RulesTest { 251 | this.suite.setDebug(debug); 252 | this.queue('debug', arguments, () => { 253 | this.suite.setDebug(debug); 254 | return Promise.resolve(); 255 | }); 256 | return this; 257 | } 258 | 259 | as(username: string): RulesTest { 260 | var client = this.suite.ensureUser(username); 261 | this.queue('as', arguments, () => { 262 | client.setDebug(this.suite.debug); 263 | this.client = client; 264 | return Promise.resolve(); 265 | }); 266 | return this; 267 | } 268 | 269 | at(opPath: string | undefined): RulesTest { 270 | this.queue('at', arguments, () => { 271 | this.path = opPath; 272 | return Promise.resolve(); 273 | }); 274 | return this; 275 | } 276 | 277 | write(obj: any): RulesTest { 278 | this.queue('write', arguments, () => { 279 | if (this.path === undefined) { 280 | return Promise.reject(new Error("Use at() function to set path to write.")); 281 | } 282 | return this.client.put(this.path, obj) 283 | .then(() => { 284 | this.status = true; 285 | }) 286 | .catch((error) => { 287 | this.status = false; 288 | this.lastError = error; 289 | }); 290 | }); 291 | return this; 292 | } 293 | 294 | push(obj: any): RulesTest { 295 | this.queue('write', arguments, () => { 296 | if (this.path === undefined) { 297 | return Promise.reject(new Error("Use at() function to set path to push.")); 298 | } 299 | let path = this.path; 300 | if (path.slice(-1)[0] !== '/') { 301 | path += '/'; 302 | } 303 | path += rest.generatePushID(); 304 | return this.client.put(path, obj) 305 | .then(() => { 306 | this.status = true; 307 | }) 308 | .catch((error) => { 309 | this.status = false; 310 | this.lastError = error; 311 | }); 312 | }); 313 | return this; 314 | } 315 | 316 | read(): RulesTest { 317 | this.queue('read', arguments, () => { 318 | if (this.path === undefined) { 319 | return Promise.reject(new Error("Use at() function to set path to read.")); 320 | } 321 | return this.client.get(this.path) 322 | .then(() => { 323 | this.status = true; 324 | }) 325 | .catch((error) => { 326 | this.status = false; 327 | this.lastError = error; 328 | }); 329 | }); 330 | return this; 331 | } 332 | 333 | succeeds(message: string): RulesTest { 334 | this.queue('succeeds', arguments, () => { 335 | assert(this.status === true, 336 | this.messageFormat(message + " (should have succeed)\n" + this.lastError)); 337 | this.good(message); 338 | this.status = undefined; 339 | return Promise.resolve(); 340 | }); 341 | return this; 342 | } 343 | 344 | fails(message: string): RulesTest { 345 | this.queue('fails', arguments, () => { 346 | assert(this.status === false, 347 | this.messageFormat(message + " (should have failed)")); 348 | this.good(message); 349 | this.status = undefined; 350 | return Promise.resolve(); 351 | }); 352 | return this; 353 | } 354 | 355 | private good(message: string): void { 356 | this.log(message + " (Correct)"); 357 | } 358 | 359 | private log(message: string): void { 360 | if (this.suite.debug) { 361 | console.log(this.messageFormat(message)); 362 | } 363 | } 364 | 365 | messageFormat(message: string): string { 366 | return this.suite.suiteName + "." + this.testName + " " + message; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/test/ast-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {assert} from 'chai'; 17 | import * as helper from './test-helper'; 18 | 19 | import * as bolt from '../bolt'; 20 | let parse = bolt.parse; 21 | import * as ast from '../ast'; 22 | 23 | suite("Abstract Syntax Tree (AST)", function() { 24 | suite("Left Associative Operators (AND OR)", function() { 25 | var t = ast.boolean(true); 26 | var f = ast.boolean(false); 27 | var v = ast.variable; 28 | var and = ast.and; 29 | var or = ast.or; 30 | var a = v('a'); 31 | var b = v('b'); 32 | var c = v('c'); 33 | var d = v('d'); 34 | 35 | var tests = [ 36 | { data: [], 37 | expect: {and: t, or: f} }, 38 | { data: [t], 39 | expect: {and: t, or: t} }, 40 | { data: [f], 41 | expect: {and: f, or: f} }, 42 | { data: [f, t], 43 | expect: {and: f, or: t} }, 44 | { data: [t, f], 45 | expect: {and: f, or: t} }, 46 | { data: [t, f, t], 47 | expect: {and: f, or: t} }, 48 | { data: [f, t, f], 49 | expect: {and: f, or: t} }, 50 | { data: [a], 51 | expect: {and: a, or: a} }, 52 | { data: [a, t], 53 | expect: {and: a, or: t} }, 54 | { data: [a, f], 55 | expect: {and: f, or: a} }, 56 | { data: [t, a], 57 | expect: {and: a, or: t} }, 58 | { data: [f, a], 59 | expect: {and: f, or: a} }, 60 | { data: [t, a, f], 61 | expect: {and: f, or: t} }, 62 | { data: [f, a, t], 63 | expect: {and: f, or: t} }, 64 | { data: [a, f, a], 65 | expect: {and: f, or: ast.or(a, a)} }, 66 | { data: [a, t, a], 67 | expect: {and: and(a, a), or: t} }, 68 | { data: [and(a, b), and(c, d)], 69 | expect: {and: and(and(and(a, b), c), d), 70 | or: or(and(a, b), and(c, d))} }, 71 | { data: [or(a, b), or(c, d)], 72 | expect: {and: and(or(a, b), or(c, d)), 73 | or: or(or(or(a, b), c), d)} }, 74 | ]; 75 | 76 | helper.dataDrivenTest(tests, function(data: ast.Exp[], expect: any) { 77 | assert.deepEqual(ast.andArray(data), expect.and, 'AND'); 78 | assert.deepEqual(ast.orArray(data), expect.or, 'OR'); 79 | }, helper.expFormat); 80 | }); 81 | 82 | suite("Flatten", function() { 83 | var v = ast.variable; 84 | var and = ast.and; 85 | var a = v('a'); 86 | var b = v('b'); 87 | var c = v('c'); 88 | var d = v('d'); 89 | 90 | var tests = [ 91 | { data: a, 92 | expect: [a] }, 93 | { data: and(a, b), 94 | expect: [a, b] }, 95 | { data: and(a, b), 96 | expect: [a, b] }, 97 | { data: and(and(a, b), c), 98 | expect: [a, b, c] }, 99 | { data: and(a, and(b, c)), 100 | expect: [a, b, c] }, 101 | { data: and(and(a, b), and(c, d)), 102 | expect: [a, b, c, d] }, 103 | ]; 104 | 105 | helper.dataDrivenTest(tests, function(data: ast.Exp, expect: ast.Exp[]) { 106 | var result = ast.flatten('&&', data); 107 | assert.deepEqual(result, expect); 108 | }, helper.expFormat); 109 | }); 110 | 111 | suite("isIdentifierString", function() { 112 | let tests = [ 113 | [ast.string('hi'), true], 114 | [ast.string('Hi'), true], 115 | [ast.string('a'), true], 116 | [ast.string('A'), true], 117 | [ast.string('hiThere'), true], 118 | [ast.string('H123'), true], 119 | [ast.string('$id'), true], 120 | [ast.string('a$id'), false], 121 | [ast.string('a b'), false], 122 | [ast.string('0H123'), false], 123 | [ast.boolean(true), false], 124 | [ast.number(123), false], 125 | ['hi', false], 126 | ]; 127 | 128 | helper.dataDrivenTest(tests, function(data: ast.Exp, expect: boolean) { 129 | var result = ast.isIdentifierStringExp(data); 130 | assert.equal(result, expect); 131 | }, helper.expFormat); 132 | }); 133 | 134 | suite("Expression decoding.", function() { 135 | var tests = [ 136 | [ "true" ], 137 | [ "false" ], 138 | [ "1" ], 139 | [ "(1)", '1' ], 140 | [ "1.1" ], 141 | [ "+3", "3"], 142 | [ "-3" ], 143 | [ "0x2", "2" ], 144 | [ "\"string\"", "'string'" ], 145 | [ "'string'" ], 146 | [ "'st\\'ring'"], 147 | [ "'st\\ring'" ], 148 | [ "'\\u000d'", "'\\r'" ], 149 | [ "/pattern/" ], 150 | [ "/pattern/i" ], 151 | [ "/pat\\/tern/i" ], 152 | [ "a" ], 153 | [ "a.b" ], 154 | [ "a.b.c" ], 155 | 156 | [ "a['b']", 'a.b' ], 157 | [ "a[b]" ], 158 | [ "a[b][c]" ], 159 | [ "a.b[c]" ], 160 | [ "a[b].c" ], 161 | [ "(a.b)[c]", "a.b[c]" ], 162 | [ "(a(b))[c]", "a(b)[c]" ], 163 | 164 | [ "a()" ], 165 | [ "a()()" ], 166 | [ "a.b()" ], 167 | [ "a().b" ], 168 | [ "a[0]()" ], 169 | [ "a()[0]" ], 170 | 171 | [ "-a" ], 172 | [ "--a" ], 173 | [ "+a", "a"], 174 | [ "+a +b", "a + b" ], 175 | [ "-a -b", "-a - b" ], 176 | [ "-a --b", "-a - -b" ], 177 | [ "-a ---b", "-a - --b" ], 178 | [ "!a" ], 179 | [ "!!a" ], 180 | [ "!+a", '!a' ], 181 | [ "!-a" ], 182 | [ "-!a" ], 183 | [ "2 * a" ], 184 | [ "(2 * a)", '2 * a' ], 185 | [ "2 / a" ], 186 | [ "a % 2" ], 187 | [ "1 + 1" ], 188 | [ "a - 1" ], 189 | [ "a - -b" ], 190 | [ "a + b + c" ], 191 | [ "a + (b + c)" ], 192 | [ "(a + b) + c", 'a + b + c' ], 193 | [ "a + b * c" ], 194 | [ "(a + b) * c" ], 195 | [ "a < 7" ], 196 | [ "a > 7" ], 197 | [ "a <= 7" ], 198 | [ "a >= 7" ], 199 | [ "a == 3" ], 200 | [ "a != 0" ], 201 | [ "a === 3", "a == 3" ], 202 | [ "a !== 0", "a != 0" ], 203 | [ "3 * a == b" ], 204 | [ "a == 1 && b <= 2", "(a == 1 && b <= 2)" ], 205 | [ "a == 1 || b <= 2", "(a == 1 || b <= 2)" ], 206 | [ "a && b && c", "((a && b) && c)" ], 207 | [ "a || b || c", "((a || b) || c)" ], 208 | [ "a && b || c && d", "((a && b) || (c && d))" ], 209 | [ "a ? b : c" ], 210 | [ "a || b ? c : d", "(a || b) ? c : d" ], 211 | [ "(this + ' ').test(/\d+/)" ], 212 | ]; 213 | 214 | helper.dataDrivenTest(tests, function(data: string, expect: string) { 215 | // Decode to self by default 216 | expect = expect || data; 217 | var result = parse('function f() {return ' + data + ';}'); 218 | var exp = result.functions.f.body; 219 | var decode = bolt.decodeExpression(exp); 220 | assert.equal(decode, expect); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /src/test/chat-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {rulesSuite, RulesTest} from '../simulator'; 17 | let secrets = require('../../auth-secrets'); 18 | 19 | rulesSuite("Chat", function(test) { 20 | var uid = test.uid; 21 | 22 | test.database(secrets.APP, secrets.SECRET); 23 | test.rules('samples/chat'); 24 | 25 | function makeMikesRoom(rules: RulesTest): RulesTest { 26 | return rules 27 | .as('mike') 28 | .at('/rooms/mikes-room') 29 | .write({ 30 | name: "Mike's room", 31 | creator: uid('mike'), 32 | }); 33 | } 34 | 35 | test("Create and Delete Room.", function(rules) { 36 | makeMikesRoom(rules) 37 | .succeeds("Create empty room.") 38 | 39 | .as('fred') 40 | .write(null) 41 | .fails("Non-owner cannot delete room.") 42 | 43 | .as('mike') 44 | .write(null) 45 | .succeeds("Owner can delete room.") 46 | ; 47 | }); 48 | 49 | test("Forge Creator of Room.", function(rules) { 50 | rules 51 | .as('mike') 52 | .at('/rooms/mikes-room') 53 | .write({ 54 | name: "Mike's room", 55 | creator: uid('fred'), 56 | }) 57 | .fails("Can't create forged room."); 58 | }); 59 | 60 | test("Join a Room.", function(rules) { 61 | makeMikesRoom(rules) 62 | .at('/rooms/mikes-room/members/' + uid('mike')) 63 | .write({ 64 | nickname: 'Mike', 65 | isBanned: false 66 | }) 67 | .succeeds("Add self to room members.") 68 | 69 | .at('/rooms/mikes-room/members/' + uid('fred')) 70 | .write({ 71 | nickname: 'Fred', 72 | isBanned: false 73 | }) 74 | .succeeds("Creator can add other members.") 75 | 76 | .as('barney') 77 | .at('/rooms/mikes-room/members/' + uid('barney')) 78 | .write({ 79 | nickname: 'Barney', 80 | isBanned: false 81 | }) 82 | .succeeds("User can add self to a room.") 83 | 84 | .as('mike') 85 | .at('/rooms/mikes-room/members/' + uid('fred')) 86 | .write(null) 87 | .succeeds("Creator can remove a member.") 88 | ; 89 | }); 90 | 91 | test("Banning and unbanning.", function(rules) { 92 | makeMikesRoom(rules) 93 | .at('/rooms/mikes-room/members/' + uid('mike')) 94 | 95 | .as('barney') 96 | .at('/rooms/mikes-room/members/' + uid('barney')) 97 | .write({ 98 | nickname: 'Barney', 99 | isBanned: false 100 | }) 101 | .succeeds("User can add self to a room.") 102 | 103 | .as('mike') 104 | .at('/rooms/mikes-room/members/' + uid('barney') + '/isBanned') 105 | .write(true) 106 | .succeeds("Creator can ban a member.") 107 | 108 | .as('barney') 109 | .at('/rooms/mikes-room/members/' + uid('barney')) 110 | .write(null) 111 | .fails("User tries to delete self.") 112 | 113 | .as('barney') 114 | .at('/rooms/mikes-room/members/' + uid('barney')) 115 | .write({ 116 | nickname: 'Barney', 117 | isBanned: false 118 | }) 119 | .fails("User tries to rejoin") 120 | 121 | .as('barney') 122 | .at('/rooms/mikes-room/members/' + uid('barney') + '/isBanned') 123 | .write(false) 124 | .fails("User tries to unban self.") 125 | 126 | .as('mike') 127 | .at('/rooms/mikes-room/members/' + uid('barney') + '/isBanned') 128 | .write(false) 129 | .succeeds("Room creator can unban user.") 130 | ; 131 | }); 132 | 133 | test("Posting.", function(rules) { 134 | makeMikesRoom(rules) 135 | .at('/posts/mikes-room') 136 | .push({ 137 | from: uid('mike'), 138 | message: "Hello, world!", 139 | created: test.TIMESTAMP, 140 | }) 141 | .fails("Owner can't write into room until he is a member.") 142 | 143 | .at('/rooms/mikes-room/members/' + uid('mike')) 144 | .write({ 145 | nickname: 'Mike', 146 | isBanned: false 147 | }) 148 | .succeeds("Add self to room members.") 149 | 150 | .at('/posts/mikes-room') 151 | .push({ 152 | from: uid('mike'), 153 | message: "Hello, world!", 154 | created: test.TIMESTAMP, 155 | }) 156 | .succeeds("Owner-member can post to room.") 157 | 158 | .as('barney') 159 | .at('/posts/mikes-room') 160 | .push({ 161 | from: uid('barney'), 162 | message: "Hello, Mike!", 163 | created: test.TIMESTAMP, 164 | }) 165 | .fails("Non-members cannot post.") 166 | 167 | .as('barney') 168 | .at('/rooms/mikes-room/members/' + uid('barney')) 169 | .write({ 170 | nickname: 'Barney', 171 | isBanned: false 172 | }) 173 | .succeeds("User can add self to a room.") 174 | 175 | .as('barney') 176 | .at('/posts/mikes-room') 177 | .push({ 178 | from: uid('barney'), 179 | message: "Hello, Mike!", 180 | created: test.TIMESTAMP, 181 | }) 182 | .succeeds("Members can post.") 183 | 184 | .as('mike') 185 | .at('/rooms/mikes-room/members/' + uid('barney') + '/isBanned') 186 | .write(true) 187 | .succeeds("Creator can ban a member.") 188 | 189 | .as('barney') 190 | .at('/posts/mikes-room') 191 | .push({ 192 | from: uid('barney'), 193 | message: "Hello, Mike!", 194 | created: test.TIMESTAMP, 195 | }) 196 | .fails("Banned members cannot post.") 197 | ; 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /src/test/cli-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {assert} from 'chai'; 17 | import * as helper from './test-helper'; 18 | import * as proc from 'child_process'; 19 | import * as fs from 'fs'; 20 | 21 | var TMP_DIR = 'tmp/'; 22 | 23 | suite("firebase-bolt CLI", function() { 24 | suiteSetup(() => { 25 | try { 26 | fs.mkdirSync(TMP_DIR); 27 | } catch (e) { 28 | // do nothing 29 | } 30 | }); 31 | 32 | var tests = [ 33 | // Simple options tests. 34 | { data: "--help", 35 | expect: {out: /^$/, err: /helpful message/} }, 36 | { data: "--h", 37 | expect: {out: /^$/, err: /helpful message/} }, 38 | { data: "--version", 39 | expect: {out: /^Firebase Bolt v\d+\.\d+\.\d+\n$/, err: /^$/} }, 40 | { data: "--v", 41 | expect: {out: /^Firebase Bolt v\d+\.\d+\.\d+\n$/, err: /^$/} }, 42 | 43 | // Reading from stdin 44 | { label: "stdin -> stdout", 45 | data: { stdin: "path / is String;" }, 46 | expect: {out: /newData\.isString/, err: /^$/} }, 47 | { label: "stdin -> file", 48 | data: { stdin: "path / is String;", args: "--o " + TMP_DIR + "test" }, 49 | expect: {out: /^$/, err: new RegExp("^bolt: Generating " + TMP_DIR + "test")} }, 50 | 51 | // Reading from a file 52 | { data: "samples/all_access", 53 | expect: {out: /^$/, err: /^bolt: Generating samples\/all_access.json\.\.\.\n$/} }, 54 | { data: "samples/all_access.bolt", 55 | expect: {out: /^$/, err: /^bolt: Generating samples\/all_access.json\.\.\.\n$/} }, 56 | { data: "samples/all_access --output " + TMP_DIR + "all_access", 57 | expect: {out: /^$/, err: new RegExp("^bolt: Generating " + TMP_DIR + "all_access.json\\.\\.\\.\\n$")} }, 58 | { data: "samples/all_access.json", 59 | expect: {out: /^$/, err: /bolt: Cannot overwrite/} }, 60 | 61 | // Argument errors 62 | { data: "--output", 63 | expect: {out: /^$/, err: /^bolt: Missing output file name/} }, 64 | { data: "nosuchfile", 65 | expect: {out: /^$/, err: /bolt: Could not read file: nosuchfile.bolt/} }, 66 | { data: "two files", 67 | expect: {out: /^$/, err: /bolt: Can only compile a single file/} }, 68 | ]; 69 | 70 | helper.dataDrivenTest(tests, function(data, expect) { 71 | return new Promise(function(resolve, reject) { 72 | let args: string; 73 | 74 | if (typeof(data) === 'string') { 75 | args = data; 76 | } else { 77 | args = data.args || ''; 78 | } 79 | let child = proc.exec('bin/firebase-bolt ' + args, function(error, stdout, stderr) { 80 | if (expect.err) { 81 | assert.isTrue(expect.err.test(stderr), "Unexpected message: '" + stderr + "'"); 82 | } 83 | if (expect.out) { 84 | assert.isTrue(expect.out.test(stdout), "Unexpected output: '" + stdout + "'"); 85 | } 86 | resolve(); 87 | }); 88 | if (data.stdin) { 89 | child.stdin.write(data.stdin); 90 | child.stdin.end(); 91 | } 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/test/create-update-delete-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {rulesSuite} from '../simulator'; 17 | var secrets = require('../../auth-secrets'); 18 | 19 | rulesSuite("Create, Update, and Delete", function(test) { 20 | test.database(secrets.APP, secrets.SECRET); 21 | test.rules('samples/create-update-delete'); 22 | var uid = test.uid; 23 | 24 | test("Create", (rules) => { 25 | rules 26 | .at('/docs/' + uid('mike') + '/create') 27 | .write('created') 28 | .succeeds("Anyone can create.") 29 | 30 | .write('updated') 31 | .fails("No-one can update.") 32 | 33 | .write(null) 34 | .fails("No-one can delete.") 35 | 36 | .as('mike') 37 | 38 | .write('owner-updated') 39 | .fails("Owner can't update.") 40 | 41 | .write(null) 42 | .fails("Owner can't delete.") 43 | ; 44 | }); 45 | 46 | test("Update", (rules) => { 47 | rules 48 | .at('/docs/' + uid('mike') + '/update') 49 | .as('mike') 50 | .write('created') 51 | .fails("Owner can't create.") 52 | 53 | .as('admin') 54 | .write('created') 55 | .succeeds("Admin can create.") 56 | 57 | .as('anon') 58 | 59 | .write('updated') 60 | .succeeds("Anyone can update.") 61 | 62 | .write(null) 63 | .fails("No-one can delete.") 64 | 65 | .as('mike') 66 | .write('owner-updated') 67 | .succeeds("Owner can update.") 68 | 69 | .write(null) 70 | .fails("Owner can't delete.") 71 | ; 72 | }); 73 | 74 | test("Delete", (rules) => { 75 | rules 76 | .at('/docs/' + uid('mike') + '/delete') 77 | .as('admin') 78 | .write('created') 79 | .succeeds("Admin can create.") 80 | 81 | .as('anon') 82 | 83 | .write('updated') 84 | .fails("Anyone can't update.") 85 | 86 | .write(null) 87 | .succeeds("Anyone can delete.") 88 | 89 | .as('mike') 90 | .write('owner-created') 91 | .fails("Owner can't create.") 92 | 93 | .as('admin') 94 | .write('created') 95 | 96 | .as('mike') 97 | 98 | .write('owner-updated') 99 | .fails("Owner can't update.") 100 | 101 | .write(null) 102 | .succeeds("Owner can delete.") 103 | ; 104 | }); 105 | 106 | test("Owner Create and Delete", (rules) => { 107 | rules 108 | .at('/docs/' + uid('mike') + '/owner-create-delete') 109 | .as('anon') 110 | .write('created') 111 | .fails("Anyone can't create.") 112 | 113 | .as('mike') 114 | .write('created') 115 | .succeeds("Owner can create.") 116 | 117 | .as('anon') 118 | 119 | .write('updated') 120 | .succeeds("Anyone can update.") 121 | 122 | .write(null) 123 | .fails("Anyone can't delete.") 124 | 125 | .as('mike') 126 | .write(null) 127 | .succeeds("Owner can delete.") 128 | ; 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/test/firebase-rest-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as rest from '../firebase-rest'; 17 | let secrets = require('../../auth-secrets'); 18 | import {assert} from 'chai'; 19 | 20 | let TEST_LOCATION = '/rest-test'; 21 | 22 | suite("Firebase REST Tests", function() { 23 | var client = new rest.Client(secrets.APP); 24 | 25 | suiteSetup(function() { 26 | var adminClient = new rest.Client(secrets.APP, secrets.SECRET); 27 | return adminClient.put( 28 | rest.RULES_LOCATION, 29 | { 30 | rules: { 31 | ".read": true, 32 | ".write": false, 33 | "rest-test": { 34 | ".write": true 35 | } 36 | } 37 | }); 38 | }); 39 | 40 | test("Read location", function() { 41 | return client.get(TEST_LOCATION); 42 | }); 43 | 44 | test("Write data", function() { 45 | var tests = [ 46 | { location: 'string', value: 'Hello, world.' }, 47 | { location: 'integer', value: 123 }, 48 | { location: 'number', value: 123.456 }, 49 | { location: 'boolean', value: false }, 50 | { location: 'object', value: {this: 1, that: 'other'} }, 51 | { location: 'TIMESTAMP', value: rest.TIMESTAMP }, 52 | ]; 53 | var results = []>[]; 54 | for (var i = 0; i < tests.length; i++) { 55 | var t = tests[i]; 56 | results.push(client.put(TEST_LOCATION + '/types/' + t.location, t.value)); 57 | } 58 | return Promise.all(results); 59 | }); 60 | 61 | test("Invalid location", function() { 62 | return client.get('../../illegal') 63 | .catch(function(error) { 64 | return true; 65 | }); 66 | }); 67 | 68 | test("PushID", function() { 69 | let id1 = rest.generatePushID(); 70 | let id2 = rest.generatePushID(); 71 | assert.equal(id1.length, 20); 72 | assert.notEqual(id1, id2); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/test/firebase-rules-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {assert} from 'chai'; 17 | import * as rest from '../firebase-rest'; 18 | 19 | let secrets = require('../../auth-secrets'); 20 | 21 | suite("Firebase Rules Tests", function() { 22 | var client = new rest.Client(secrets.APP, secrets.SECRET); 23 | 24 | test("Write Rules", function() { 25 | return client.put( 26 | rest.RULES_LOCATION, 27 | { 28 | rules: { 29 | ".read": true, 30 | ".write": false, 31 | "rest-test": { 32 | ".write": true 33 | } 34 | } 35 | }); 36 | }); 37 | 38 | test("Read Rules", function() { 39 | return client.get(rest.RULES_LOCATION) 40 | .then(function(result: any) { 41 | assert('rules' in result); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Firebase Bolt Compiler - {{ set.title }} 5 | 6 | 7 | 16 | 17 | 18 |
19 |

Firebase Bolt Compiler - {{ set.title }}

20 | 25 |
26 | 27 |
28 | 29 | 32 | 33 | {{#set.tests}} 34 | 35 | {{/set.tests}} 36 | 37 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/test/issue-118-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {rulesSuite} from '../simulator'; 17 | var secrets = require('../../auth-secrets'); 18 | 19 | rulesSuite("Type[] | Scalar - issue 118", function(test) { 20 | test.database(secrets.APP, secrets.SECRET); 21 | test.rules('samples/issue-118'); 22 | 23 | test("Scalar tests.", function(rules) { 24 | rules 25 | // .debug() 26 | .as('mike') 27 | .at('/path/scalar') 28 | .write(1) 29 | .succeeds("Write to scalar.") 30 | .write(null) 31 | .succeeds("Deleting whole item can delete scalar.") 32 | 33 | // Needed for all subsequent tests to pass. 34 | .write(1) 35 | 36 | .at('/path/scalarOrNull') 37 | .write(1) 38 | .succeeds("Write to scalar or null.") 39 | .write(null) 40 | .succeeds("Can delete scalar or null.") 41 | ; 42 | }); 43 | 44 | test("Array tests.", function(rules) { 45 | rules 46 | // .debug() 47 | .as('mike') 48 | .at('/path/scalar') 49 | .write(1) 50 | 51 | .at('/path/array') 52 | .write([1]) 53 | .succeeds("Write to array.") 54 | .write(null) 55 | .succeeds("Deleting whole array.") 56 | .at('/path/array/999') 57 | .write(1) 58 | .succeeds("Write single entry to array.") 59 | .at('/path/array') 60 | .push(2) 61 | .succeeds("Pushes value into array.") 62 | .write("bogus") 63 | .fails("Should not be able to write non-array to array.") 64 | 65 | .at('/path/arrayOrNull/999') 66 | .write(1) 67 | .succeeds("Can write single array entry to array or null.") 68 | 69 | .at('/path/arrayOrNull') 70 | .write([1]) 71 | .succeeds("Write array to array or null.") 72 | .write(null) 73 | .succeeds("Can delete array or null.") 74 | 75 | .at('/path/arrayOrScalar') 76 | .write(1) 77 | .succeeds("Write scalar to array or scalar.") 78 | .write([1]) 79 | .succeeds("Write array to array or scalar.") 80 | ; 81 | }); 82 | 83 | test("Map tests.", function(rules) { 84 | rules 85 | // .debug() 86 | .as('mike') 87 | .at('/path/scalar') 88 | .write(1) 89 | 90 | .at('/path/map') 91 | .write([1]) 92 | .succeeds("Write to map.") 93 | .write(null) 94 | .succeeds("Deleting whole map.") 95 | .at('/path/map/key') 96 | .write(1) 97 | .succeeds("Write single entry to map.") 98 | .at('/path/map') 99 | .push(2) 100 | .succeeds("Pushes value into map.") 101 | .write("bogus") 102 | .fails("Should not be able to write non-map to map.") 103 | 104 | .at('/path/mapOrScalar') 105 | .write(1) 106 | .succeeds("Write scalar to map or scalar.") 107 | .write({"key": 2}) 108 | .succeeds("Write map to map or scalar.") 109 | ; 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/test/mail-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {rulesSuite} from '../simulator'; 17 | var secrets = require('../../auth-secrets'); 18 | 19 | rulesSuite("Mail", function(test) { 20 | var uid = test.uid; 21 | 22 | test.database(secrets.APP, secrets.SECRET); 23 | test.rules('samples/mail'); 24 | 25 | test("Inbox tests.", function(rules) { 26 | rules 27 | .as('tom') 28 | .at('/users/' + uid('bill') + '/inbox/1') 29 | .write({ 30 | from: uid('tom'), 31 | to: uid('bill'), 32 | message: 'Hi, Bill!' 33 | }) 34 | .succeeds("Normal write.") 35 | 36 | .write(null) 37 | .fails("Sender cannot delete sent message.") 38 | 39 | .write({ 40 | from: uid('tom'), 41 | to: uid('bill'), 42 | message: 'Hello, again!' 43 | }) 44 | .fails("Sender cannot overwrite.") 45 | 46 | .at('/users/' + uid('bill') + '/inbox/2') 47 | .write({ 48 | from: uid('tom'), 49 | to: uid('bill'), 50 | message: 'Hi, Bill!', 51 | spurious: 'supurious data' 52 | }) 53 | .fails("No undefined fields.") 54 | 55 | .write({ 56 | from: uid('george'), 57 | to: uid('bill'), 58 | message: 'Hi, Bill!' 59 | }) 60 | .fails("From field should be correct.") 61 | 62 | .at('/users/' + uid('bill') + '/inbox/1/message') 63 | .write("Bill gets my inheritance") 64 | .fails("Cannnot tamper with message.") 65 | 66 | .at('/users/' + uid('bill') + '/inbox/1/from') 67 | .write(uid('bill')) 68 | .fails("Cannot tamper with from field.") 69 | 70 | .as('bill') 71 | .at('/users/' + uid('bill') + '/inbox/1') 72 | .write(null) 73 | .succeeds("Receiver can delete received mail."); 74 | }); 75 | 76 | 77 | test("Outbox tests.", function(rules) { 78 | rules 79 | .as('bill') 80 | .at('/users/' + uid('bill') + '/outbox/1') 81 | .write({ 82 | from: uid('bill'), 83 | to: uid('tom'), 84 | message: "Hi, Tom!" 85 | }) 86 | .succeeds("Normal write.") 87 | 88 | .as('tom') 89 | .write(null) 90 | .fails("Receiver cannot delete outbox message.") 91 | 92 | .as('bill') 93 | 94 | .at('/users/' + uid('bill') + '/outbox/1/message') 95 | .write("Bill gets my inheritance.") 96 | .fails("Sender cannot tamper with outbox message.") 97 | 98 | .at('/users/' + uid('bill') + '/outbox/1/from') 99 | .write('bill') 100 | .fails("Can't do a partial overwrite - even if same data.") 101 | 102 | .as('bill') 103 | .at('/users/' + uid('bill') + '/outbox/2') 104 | .write({ 105 | from: 'joe', 106 | to: 'tom', 107 | message: "Hi, Tom!" 108 | }) 109 | .fails("From field must be correct.") 110 | 111 | .write({ 112 | from: 'bill', 113 | to: 'tom', 114 | message: "Hi, Tom!", 115 | spurious: "spurious" 116 | }) 117 | .fails("No undefined fields.") 118 | 119 | .at('/users/' + uid('bill') + '/outbox/1') 120 | .write(null) 121 | .succeeds("Sender can delete sent mail in outbox."); 122 | }); 123 | 124 | test("Read permissions.", function(rules) { 125 | rules 126 | .as('bill') 127 | .at('/users/' + uid('bill') + '/outbox/1') 128 | .write({ 129 | from: uid('bill'), 130 | to: uid('tom'), 131 | message: 'Hi, Tom!' 132 | }) 133 | .succeeds("Normal write.") 134 | 135 | .as('tom') 136 | .at('/users/' + uid('bill') + '/inbox/1') 137 | .write({ 138 | from: uid('tom'), 139 | to: uid('bill'), 140 | message: 'Hi, Bill!' 141 | }) 142 | 143 | .as('bill') 144 | .at('/users/' + uid('bill') + '/inbox/1') 145 | .read() 146 | .succeeds("Can read own inbox.") 147 | 148 | .at('/users/' + uid('bill') + '/outbox/1') 149 | .read() 150 | .succeeds("Can read own outbox.") 151 | 152 | .as('tom') 153 | .at('/users/' + uid('bill') + '/inbox/1') 154 | .read() 155 | .fails("Can't read Bill's inbox.") 156 | 157 | .at('/users/' + uid('bill') + '/outbox/1') 158 | .read() 159 | .fails("Can't read Bills outbox."); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /src/test/regexp-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {rulesSuite} from '../simulator'; 17 | let secrets = require('../../auth-secrets'); 18 | 19 | rulesSuite("RegExp", function(test) { 20 | test.database(secrets.APP, secrets.SECRET); 21 | test.rules('samples/regexp'); 22 | 23 | test("SocialSecurity", (rules) => { 24 | rules 25 | .at('/ss') 26 | .write('000-00-0000') 27 | .succeeds("All zeros.") 28 | 29 | .write('123-45-6789') 30 | .succeeds("All numbers.") 31 | 32 | .write('000-0a-0000') 33 | .fails("Contains letter.") 34 | 35 | .write('000-00-00000') 36 | .fails("Too long.") 37 | 38 | .write('000-0-000') 39 | .fails("Too short.") 40 | 41 | .write('00000000') 42 | .fails("Missing dashes.") 43 | ; 44 | }); 45 | 46 | test("IntegerString", (rules) => { 47 | rules 48 | .at('/integer') 49 | .write('0') 50 | .succeeds("Zero.") 51 | 52 | .write('123') 53 | .succeeds("Example.") 54 | 55 | .write('-123') 56 | .succeeds("Negative Example.") 57 | 58 | .write('--123') 59 | .fails("Double negative.") 60 | 61 | .write('') 62 | .fails("Empty string.") 63 | 64 | .write('a') 65 | .fails("Alphabetic.") 66 | 67 | .write(' 0') 68 | .fails("Has spaces.") 69 | 70 | .write('0.0') 71 | .fails("Has decimal.") 72 | ; 73 | }); 74 | 75 | test("FloatString", (rules) => { 76 | rules 77 | .at('/float') 78 | .write('0.0') 79 | .succeeds("Zero.") 80 | 81 | .write('123.456') 82 | .succeeds("Fixed point number.") 83 | 84 | .write('-123.456') 85 | .succeeds("Negative ixed point number.") 86 | 87 | .write('.1') 88 | .succeeds("No leading digits.") 89 | 90 | .write('1.') 91 | .succeeds("No trailing digits.") 92 | 93 | .write('-.1') 94 | .succeeds("Negative fraction only.") 95 | 96 | .write('.') 97 | .fails("Just decimal point.") 98 | 99 | .write('0') 100 | .succeeds("Zero.") 101 | 102 | .write('') 103 | .fails("Empty string.") 104 | 105 | .write('a') 106 | .fails("Alphabetic.") 107 | 108 | .write(' 0') 109 | .fails("Has spaces.") 110 | ; 111 | }); 112 | 113 | test("Integer", (rules) => { 114 | rules 115 | .at('/int') 116 | .write(0) 117 | .succeeds("Zero.") 118 | 119 | .write(0.0) 120 | .succeeds("Floating Zero.") 121 | 122 | .write(123) 123 | .succeeds("Example.") 124 | 125 | .write(-123) 126 | .succeeds("Negative example.") 127 | 128 | .write(1.1) 129 | .fails("No fractional part allowed.") 130 | 131 | .write('0') 132 | .fails("String.") 133 | ; 134 | }); 135 | 136 | test("Alpha", (rules) => { 137 | rules 138 | .at('/alpha') 139 | .write('a') 140 | .succeeds("Alpha") 141 | 142 | .write('A') 143 | .succeeds("Alpha") 144 | 145 | .write("hello") 146 | .succeeds("Word.") 147 | 148 | .write("123") 149 | .fails("Numeric.") 150 | 151 | .write(1) 152 | .fails("Number.") 153 | 154 | .write(true) 155 | .fails("Boolean.") 156 | 157 | .write("hello, world") 158 | .fails("Non-alpha.") 159 | ; 160 | }); 161 | 162 | test("Year", (rules) => { 163 | rules 164 | .at('/year') 165 | .write('2015') 166 | .succeeds("This year.") 167 | 168 | .write('1900') 169 | .succeeds("Earliest year.") 170 | 171 | .write('1999') 172 | .succeeds("Latest in 20th century.") 173 | 174 | .write('2099') 175 | .succeeds("Latest in 21th century.") 176 | 177 | .write('2015 ') 178 | .fails("Extra space.") 179 | 180 | .write('2100') 181 | .fails("Distant future.") 182 | 183 | .write(1960) 184 | .fails("Number.") 185 | 186 | .write('') 187 | .fails("Empty string.") 188 | ; 189 | }); 190 | 191 | test("ISODate", (rules) => { 192 | rules 193 | .at('/date') 194 | .write('2015-11-20') 195 | .succeeds("Today.") 196 | 197 | .write('1900-01-01') 198 | .succeeds("Earliest date.") 199 | 200 | .write('2099-12-31') 201 | .succeeds("Latest date.") 202 | 203 | .write('1899-12-31') 204 | .fails("Too early date.") 205 | 206 | .write('2100-01-01') 207 | .fails("Too late date.") 208 | 209 | .write('') 210 | .fails("Empty string.") 211 | ; 212 | }); 213 | 214 | test("Slug", (rules) => { 215 | rules 216 | .at('/slug') 217 | .write('this-is-a-slug') 218 | .succeeds("Typical slug text.") 219 | 220 | .write('numbers-2016-ok') 221 | .succeeds("Number are ok.") 222 | 223 | .write('double--hyphen') 224 | .fails("Double hyphen not ok.") 225 | 226 | .write('-leading-hyphen') 227 | .fails("Leading hyphen not ok.") 228 | 229 | .write('trailing-hyphen-') 230 | .fails("Trailing hyphen not ok.") 231 | 232 | .write('nohyphen') 233 | .fails("Must have at least one hyphen.") 234 | 235 | .write('no-Upper') 236 | .fails("No upper case.") 237 | 238 | .write('no-special&-char') 239 | .fails("No special characters.") 240 | 241 | .write('no spaces') 242 | .fails("No spaces allowed.") 243 | ; 244 | }); 245 | 246 | test("Domain", (rules) => { 247 | rules 248 | .at('/domain') 249 | .write('google.com') 250 | .succeeds("Simple domain.") 251 | 252 | .write('google-com') 253 | .fails("Not a domain.") 254 | ; 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /src/test/sample-files.ts: -------------------------------------------------------------------------------- 1 | export let samples = [ 2 | "all_access", 3 | "chat", 4 | "children-by-nesting", 5 | "children", 6 | "create-update-delete", 7 | "functional", 8 | "generics", 9 | "groups", 10 | "issue-111", 11 | "issue-118", 12 | "issue-136", 13 | "issue-169", 14 | "issue-232", 15 | "issue-97", 16 | "mail", 17 | "map-scalar", 18 | "multi-update", 19 | "regexp", 20 | "serialized", 21 | "type-extension", 22 | "user-security", 23 | "userdoc", 24 | ]; 25 | -------------------------------------------------------------------------------- /src/test/test-helper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as util from '../util'; 18 | import * as ast from '../ast'; 19 | import * as logger from '../logger'; 20 | 21 | export interface ObjectSpec { 22 | label?: string; 23 | data: any; 24 | expect?: any; 25 | }; 26 | 27 | export type ArraySpec = [any, any]; 28 | 29 | export type ValueSpec = any; 30 | 31 | export type Spec = ObjectSpec | ArraySpec | ValueSpec; 32 | 33 | export type TestFunction = (data: any, expect: any, spec: Spec) => void; 34 | 35 | export type FormatFunction = (data: any) => string; 36 | 37 | /* 38 | * Run data drive test with tests is one of these formats: 39 | * [ { label: (opt) , data: , expect: (opt) }, ... ] 40 | * [ [ , ], ... ] 41 | * [ scalar, ... ] 42 | * 43 | * Calls testIt(data, expect) for each test. 44 | */ 45 | export function dataDrivenTest(tests: Spec[], testIt: TestFunction, formatter = format) { 46 | var data: any; 47 | var expect: any; 48 | var label: string; 49 | 50 | for (var i = 0; i < tests.length; i++) { 51 | // Not Array or Object 52 | if (typeof tests[i] !== 'object') { 53 | label = formatter(tests[i]); 54 | data = tests[i]; 55 | expect = undefined; 56 | } else { 57 | data = tests[i].data; 58 | if (data === undefined) { 59 | data = tests[i][0]; 60 | } 61 | if (data === undefined) { 62 | data = tests[i]; 63 | } 64 | if (util.isType(data, 'object') && 'expect' in data) { 65 | data = util.extend({}, data); 66 | delete data.expect; 67 | } 68 | expect = tests[i].expect || tests[i][1]; 69 | label = tests[i].label; 70 | if (label === undefined) { 71 | if (expect !== undefined) { 72 | label = formatter(data) + " => " + formatter(expect); 73 | } else { 74 | label = formatter(data); 75 | } 76 | } 77 | } 78 | 79 | setup(() => { 80 | logger.reset(); 81 | logger.silent(); 82 | }); 83 | teardown(() => { 84 | logger.reset(); 85 | }); 86 | test(label, testIt.bind(undefined, data, expect, tests[i])); 87 | } 88 | } 89 | 90 | function format(o: any): string { 91 | switch (util.typeOf(o)) { 92 | case 'regexp': 93 | return o.toString(); 94 | default: 95 | return JSON.stringify(o); 96 | } 97 | } 98 | 99 | export function expFormat(x: any): string { 100 | if (util.isType(x, 'array')) { 101 | return '[' + x.map(expFormat).join(', ') + ']'; 102 | } 103 | if (util.isType(x, 'object')) { 104 | if ('type' in x) { 105 | return ast.decodeExpression(x); 106 | } 107 | var result = '{'; 108 | var sep = ''; 109 | for (var prop in x) { 110 | if (!x.hasOwnProperty(prop)) { 111 | continue; 112 | } 113 | result += sep + expFormat(x[prop]); 114 | sep = ', '; 115 | } 116 | result += '}'; 117 | return result; 118 | } 119 | return JSON.stringify(x); 120 | } 121 | -------------------------------------------------------------------------------- /src/test/util-test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {assert} from 'chai'; 17 | import * as helper from './test-helper'; 18 | import * as util from '../util'; 19 | 20 | suite("Util", function() { 21 | suite("pruneEmptyChildren", function() { 22 | class T { 23 | public x = 'dummy'; 24 | } 25 | 26 | var tests = [ 27 | [ {}, {} ], 28 | [ {a: 1}, {a: 1} ], 29 | [ {a: {}}, {} ], 30 | [ {a: 1, b: {}}, {a: 1} ], 31 | [ {a: []}, {a: []} ], 32 | [ {a: new T()}, {a: new T()} ], 33 | [ {a: {a: {a: {}}}}, {} ], 34 | [ {a: {a: {a: {}, b: 1}}}, {a: {a: {b: 1}}} ], 35 | [ {a: 1, b: undefined}, {a: 1} ], 36 | ]; 37 | 38 | helper.dataDrivenTest(tests, function(data: any, expect: any) { 39 | util.pruneEmptyChildren(data); 40 | assert.deepEqual(data, expect); 41 | }); 42 | }); 43 | 44 | suite("pruneEmptyChildren", function() { 45 | var tests = [ 46 | [ {}, {} ], 47 | [ {a: 1}, {a: 1} ], 48 | [ {a: 1, dm: 2}, {a: 1} ], 49 | [ {a: 1, b: {dm: 2, c: 3}}, {a: 1, b: {c: 3}} ], 50 | ]; 51 | 52 | helper.dataDrivenTest(tests, function(data: any, expect: any) { 53 | util.deletePropName(data, 'dm'); 54 | assert.deepEqual(data, expect); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export type Object = { 17 | [prop: string]: any 18 | }; 19 | export function extend(dest: Object, ...srcs: Object[]): Object { 20 | var i: number; 21 | var source: any; 22 | var prop: string; 23 | 24 | if (dest === undefined) { 25 | dest = {}; 26 | } 27 | for (i = 0; i < srcs.length; i++) { 28 | source = srcs[i]; 29 | for (prop in source) { 30 | if (source.hasOwnProperty(prop)) { 31 | dest[prop] = source[prop]; 32 | } 33 | } 34 | } 35 | 36 | return dest; 37 | } 38 | 39 | export function copyArray(arg: ArrayLike): any[] { 40 | return Array.prototype.slice.call(arg); 41 | } 42 | 43 | var baseTypes = [ 44 | 'number', 'string', 'boolean', 'array', 'function', 'date', 'regexp', 45 | 'arguments', 'undefined', 'null' 46 | ]; 47 | 48 | function internalType(value: any): string { 49 | return Object.prototype.toString.call(value) 50 | .match(/\[object (.*)\]/)[1] 51 | .toLowerCase(); 52 | } 53 | 54 | export function isType(value: any, type: string): boolean { 55 | return typeOf(value) === type; 56 | } 57 | 58 | // Return one of the baseTypes as a string 59 | export function typeOf(value: any): string { 60 | if (value === undefined) { 61 | return 'undefined'; 62 | } 63 | if (value === null) { 64 | return 'null'; 65 | } 66 | var type = internalType(value); 67 | if (!arrayIncludes(baseTypes, type)) { 68 | type = typeof value; 69 | } 70 | return type; 71 | } 72 | 73 | export function isThenable(obj: any): boolean { 74 | return typeOf(obj) === 'object' && 'then' in obj && 75 | typeof (obj.then) === 'function'; 76 | } 77 | 78 | // Converts a synchronous function to one allowing Promises 79 | // as arguments and returning a Promise value. 80 | // 81 | // fn(U, V, ...): T => fn(U | Promise, V | Promise, ...): Promise 82 | export function lift(fn: (...args: any[]) => T): (...args: any[]) => 83 | Promise { 84 | return function(...args: any[]): Promise { 85 | return Promise.all(args).then((values: any[]) => { 86 | return fn.apply(undefined, values); 87 | }); 88 | }; 89 | } 90 | 91 | // Converts an asynchronous function to one allowing Promises 92 | // as arguments. 93 | // 94 | // fn(U, V, ...): Promise => fn(U | Promise, V | Promise, ...): 95 | // Promise 96 | export let liftArgs: (fn: (...args: any[]) => Promise) => 97 | ((...args: any[]) => Promise) = lift; 98 | 99 | export let getProp = lift((obj, prop) => obj[prop]); 100 | 101 | export function ensureExtension(fileName: string, extension: string): string { 102 | if (fileName.indexOf('.') === -1) { 103 | return fileName + '.' + extension; 104 | } 105 | return fileName; 106 | } 107 | 108 | export function replaceExtension(fileName: string, extension: string): string { 109 | return fileName.replace(/\.[^\.]*$/, '.' + extension); 110 | } 111 | 112 | export function prettyJSON(o: any): string { 113 | return JSON.stringify(o, null, 2); 114 | } 115 | 116 | function deepExtend(target: Object, source: Object): void { 117 | for (var prop in source) { 118 | if (!source.hasOwnProperty(prop)) { 119 | continue; 120 | } 121 | 122 | if (target[prop] !== undefined) { 123 | throw new Error('Property overwrite: ' + prop); 124 | } 125 | 126 | if (isType(source[prop], 'object')) { 127 | target[prop] = {}; 128 | deepExtend(target[prop], source[prop]); 129 | } else { 130 | target[prop] = source[prop]; 131 | } 132 | } 133 | } 134 | 135 | export function deepLookup(o: Object, path: string[]): Object|undefined { 136 | let result = o; 137 | 138 | for (let i = 0; i < path.length; i++) { 139 | if (result === undefined) { 140 | return undefined; 141 | } 142 | result = result[path[i]]; 143 | } 144 | return result; 145 | } 146 | 147 | // Like JSON.stringify - but for single-quoted strings instead of double-quoted 148 | // ones. This just makes the compiled rules much easier to read. 149 | 150 | // Quote all control characters, slash, single quotes, and non-ascii printables. 151 | var quotableCharacters = /[\u0000-\u001f\\\'\u007f-\uffff]/g; 152 | var specialQuotes = <{[c: string]: string}>{ 153 | '\'': '\\\'', 154 | '\b': '\\b', 155 | '\t': '\\t', 156 | '\n': '\\n', 157 | '\f': '\\f', 158 | '\r': '\\r' 159 | }; 160 | 161 | export function quoteString(s: string): string { 162 | s = s.replace(quotableCharacters, function(c) { 163 | if (specialQuotes[c]) { 164 | return specialQuotes[c]; 165 | } 166 | return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); 167 | }); 168 | return '\'' + s + '\''; 169 | } 170 | 171 | export function arrayIncludes(a: any[], e: any): boolean { 172 | return a.indexOf(e) !== -1; 173 | } 174 | 175 | // Like Python list.extend 176 | export function extendArray(target: any[], src: any[]) { 177 | if (target === undefined) { 178 | target = []; 179 | } 180 | Array.prototype.push.apply(target, src); 181 | return target; 182 | } 183 | 184 | export function or(target: any, src: any) { 185 | if (target === undefined) { 186 | return false; 187 | } 188 | return target || src; 189 | } 190 | 191 | export function ensureObjectPath(obj: Object, parts: string[]): Object { 192 | for (var i = 0; i < parts.length; i++) { 193 | var name = parts[i]; 194 | if (!(name in obj)) { 195 | obj[name] = {}; 196 | } 197 | obj = obj[name]; 198 | } 199 | return obj; 200 | } 201 | 202 | // Remove all empty, '{}', children and undefined - returns true iff obj is 203 | // empty. 204 | export function pruneEmptyChildren(obj: Object): boolean { 205 | if (obj === undefined) { 206 | return true; 207 | } 208 | if (obj.constructor !== Object) { 209 | return false; 210 | } 211 | var hasChildren = false; 212 | for (var prop in obj) { 213 | if (!obj.hasOwnProperty(prop)) { 214 | continue; 215 | } 216 | if (pruneEmptyChildren(obj[prop])) { 217 | delete obj[prop]; 218 | } else { 219 | hasChildren = true; 220 | } 221 | } 222 | return !hasChildren; 223 | } 224 | 225 | export function deletePropName(obj: Object, name: string) { 226 | if (obj.constructor !== Object) { 227 | return; 228 | } 229 | for (var prop in obj) { 230 | if (!obj.hasOwnProperty(prop)) { 231 | continue; 232 | } 233 | if (prop === name) { 234 | delete obj[prop]; 235 | } else { 236 | deletePropName(obj[prop], name); 237 | } 238 | } 239 | } 240 | 241 | export function formatColumns(indent: number, lines: string[][]): string[] { 242 | let result: string[] = []; 243 | let columnSize = []; 244 | 245 | for (let i = 0; i < lines.length; i++) { 246 | let line = lines[i]; 247 | for (let j = 0; j < line.length; j++) { 248 | if (columnSize[j] === undefined) { 249 | columnSize[j] = 0; 250 | } 251 | columnSize[j] = Math.max(columnSize[j], line[j].length); 252 | } 253 | } 254 | 255 | var prefix = repeatString(' ', indent); 256 | var s: string; 257 | for (let i = 0; i < lines.length; i++) { 258 | let line = lines[i]; 259 | let sep = ''; 260 | s = ''; 261 | for (let j = 0; j < line.length; j++) { 262 | if (j === 0) { 263 | s = prefix; 264 | } 265 | if (j === line.length - 1) { 266 | s += sep + line[j]; 267 | } else { 268 | s += sep + fillString(line[j], columnSize[j]); 269 | } 270 | sep = ' '; 271 | } 272 | result.push(s); 273 | } 274 | 275 | return result; 276 | } 277 | 278 | function repeatString(s: string, n: number): string { 279 | return new Array(n + 1).join(s); 280 | } 281 | 282 | function fillString(s: string, n: number): string { 283 | let padding = n - s.length; 284 | if (padding > 0) { 285 | s += repeatString(' ', padding); 286 | } 287 | return s; 288 | } 289 | -------------------------------------------------------------------------------- /tools/bash-helper: -------------------------------------------------------------------------------- 1 | # bash-helper --- Helper for bash scripting with command line flags (utililty). 2 | # (source this file from your bash script) 3 | # 4 | # Copyright 2015 Google Inc. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # Adds --help flag and calls your usage function (and exits). 19 | # 20 | # Example usage: 21 | # 22 | # ALL_FLAGS="list of flags" 23 | # list_help="Help text for list flag." 24 | # of_help="Help for 'of' flag." 25 | # flags_help="Help for 'flags' flag." 26 | # usage () { 27 | # echo "My command description." 28 | # usage_helper 29 | # echo "extra command args" 30 | # show_help " --" $ALL_FLAGS 31 | # 32 | # } 33 | # source $TOOLS_DIR/bash-helper 34 | # 35 | # On return, list_flag is "true" if set in command line. 36 | # All flags must precede all non-flags (and args are consumed). 37 | 38 | ALL_FLAGS="$ALL_FLAGS help" 39 | help_help="Display this helpful message." 40 | 41 | show_help () { 42 | local prefix="$1"; shift 43 | local items="$@" 44 | local item 45 | local help_text 46 | 47 | for item in $items; do 48 | help_text="$(eval echo \$${item}_help)" 49 | if [[ $help_text != "" ]]; then 50 | echo -e "${prefix}${item} - $help_text" 51 | fi 52 | done 53 | } 54 | 55 | usage_helper () { 56 | echo -n "Usage: $(basename $0) " 57 | for flag in $ALL_FLAGS; do 58 | echo -n "--$flag " 59 | done 60 | } 61 | 62 | is_in_list () { 63 | local word="$1"; shift 64 | local list="$@" 65 | 66 | for test in $list; do 67 | if [[ $test == $word ]]; then 68 | return 0 69 | fi 70 | done 71 | return 1 72 | } 73 | 74 | # 75 | # Parse all the flags and consume the arguments. 76 | # 77 | while [[ $1 == "--"* ]]; do 78 | flag=${1:2} 79 | if ! is_in_list $flag $ALL_FLAGS; then 80 | echo "Unknown flag $1" 81 | usage 82 | exit 1 83 | fi 84 | eval "${1:2}_flag=true"; shift 85 | done 86 | 87 | if [ $help_flag ]; then 88 | usage 89 | exit 0 90 | fi 91 | -------------------------------------------------------------------------------- /tools/browser-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # browser-tests --- Run unit tests in a browser environment. 3 | set -e 4 | cd $PROJ_DIR 5 | 6 | gulp clean lint build-browser-tests 7 | web-server start 8 | -------------------------------------------------------------------------------- /tools/compare-sample: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # compare-sample --- Compare output of bolt compiler with json target. 3 | 4 | set -e 5 | 6 | cd $PROJ_DIR/samples 7 | 8 | files=() 9 | 10 | if [[ "$1" == "" ]]; then 11 | echo "Usage: compare-sample --all | " 12 | exit 1 13 | fi 14 | 15 | if [[ "$1" == "--all" ]]; then 16 | for file in *.bolt; do 17 | files+=(${file%%.bolt}) 18 | done 19 | else 20 | files+=("$1") 21 | fi 22 | 23 | for file in "${files[@]}"; do 24 | echo "Comparing firebase-bolt $file.bolt ~ $file.json:" 25 | diff <(firebase-bolt < "$file.bolt" | flatten-json.py) <(flatten-json.py < "$file.json") || true 26 | done 27 | -------------------------------------------------------------------------------- /tools/configure-project: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # configure-project --- Configure repository dependencies for building and tests. 3 | set -e 4 | 5 | cd $PROJ_DIR 6 | 7 | echo "Installing dependencies ..." 8 | npm install 9 | 10 | echo -e "\nSelect a Firebase application name where tests will be executed." 11 | echo "WARNING: ALL DATA IN THIS DATABASE WILL BE REMOVED BY RUNNING TESTS." 12 | echo " Please create an application specifically for testing." 13 | echo " DO NOT use the name of your production database!!!" 14 | 15 | ensure-secret.py auth-secrets.js 16 | 17 | # Show outdated modules 18 | npm outdated 19 | -------------------------------------------------------------------------------- /tools/debug-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # debug-tests --- Run unit tests in mocha under node debugger 3 | 4 | cd $PROJ_DIR 5 | mocha lib/test --ui tdd debug "$@" 6 | -------------------------------------------------------------------------------- /tools/ensure-secret.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # ensure-secret.py --- Initialize a js module file with Firebase secret. 3 | # 4 | # Copyright 2015 Google Inc. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | from __future__ import print_function 19 | 20 | import argparse 21 | import webbrowser 22 | import json 23 | 24 | HOST = "firebaseio.com" 25 | 26 | 27 | def main(): 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument("file", help="Secrets file") 30 | args = parser.parse_args() 31 | 32 | try: 33 | with open(args.file) as f: 34 | print( 35 | "Secrets file, {filename} already exists.".format(filename=args.file)) 36 | return 37 | except IOError: 38 | print("{0} does not exist.".format(args.file)) 39 | app_name = raw_input("Firebase app: ") 40 | secrets_url = "https://console.firebase.google.com/project/{app}/settings/serviceaccounts/databasesecrets".format( 41 | app=app_name) 42 | print("Copy app secret from %s ..." % secrets_url) 43 | webbrowser.open(secrets_url) 44 | print("(if using Firebase 2.0 database, find app secret at: " 45 | "https://{app}.{host}?page=Admin)"\ 46 | .format(app=app_name, host=HOST)) 47 | secret = raw_input("Firebase Secret: ") 48 | data = {"APP": app_name, "SECRET": secret} 49 | with open(args.file, "w") as f: 50 | f.write("module.exports = {json};\n".format( 51 | json=json.dumps(data, indent=2, separators=(",", ": ")))) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /tools/flatten-json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import print_function 18 | 19 | import sys 20 | import json 21 | 22 | 23 | def main(): 24 | data = json.load(sys.stdin) 25 | lines = [] 26 | extract_values(data, '', lines) 27 | lines.sort() 28 | print('\n'.join(lines)) 29 | 30 | 31 | def extract_values(j, base, lines): 32 | if type(j) is dict: 33 | for p in j: 34 | extract_values(j[p], base + '/' + p, lines) 35 | return 36 | 37 | if type(j) is list: 38 | for i in range(len(j)): 39 | extract_values(j[i], base + '/' + str(i), lines) 40 | return 41 | 42 | lines.append(base + ': ' + str(j)) 43 | 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /tools/regenerate-sample-files: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # regenerate-sample-files --- Recreate the sample-files.ts file for test. 3 | 4 | set -e 5 | 6 | DEST="$PROJ_DIR/src/test/sample-files.ts" 7 | 8 | echo "export let samples = [" > "$DEST" 9 | 10 | cd "$PROJ_DIR/samples" 11 | 12 | for file in *.bolt; do 13 | base="${file%.*}" 14 | echo " \"$base\"," >> $DEST 15 | done 16 | 17 | echo "];" >> $DEST 18 | -------------------------------------------------------------------------------- /tools/run-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # run-tests --- Run unit tests in mocha - passing commandline options. 3 | 4 | set -e 5 | 6 | cd $PROJ_DIR 7 | 8 | if [[ ! -d "node_modules" || ! -f "auth-secrets.js" ]]; then 9 | echo "Project not yet configured to run tests." 10 | echo "Run: configure-project command." 11 | exit 1 12 | fi 13 | 14 | gulp clean 15 | if gulp lint build; then 16 | mocha lib/test --ui tdd --require "source-map-support/register" "$@" 17 | fi 18 | -------------------------------------------------------------------------------- /tools/use: -------------------------------------------------------------------------------- 1 | # Initialize this environment by executing: 2 | # 3 | # $ source use 4 | # 5 | export PROJECT=firebase-bolt 6 | export TOOLS_DIR="$(unset CDPATH; cd $(dirname ${BASH_SOURCE[0]}) && pwd)" 7 | source $TOOLS_DIR/usefuncs 8 | 9 | # 10 | # Add build tools to PATH 11 | # 12 | PATH="$PROJ_DIR/bin:$(cd $PROJ_DIR; npm bin):$PATH" 13 | CDPATH="$PROJ_DIR/src:$CDPATH" 14 | export NODE_PATH="$PROJ_DIR/lib" 15 | 16 | # 17 | # Non-interactive shells - exit here. 18 | # 19 | # Allow bash scripts to use source $TOOLS_DIR/use to assign environment 20 | # variables only via: 21 | # 22 | # TOOLS_DIR="$(unset CDPATH; cd $(dirname ${BASH_SOURCE[0]}) && pwd)" 23 | # source $TOOLS_DIR/use 24 | # 25 | if [ -z "$PS1" ]; then 26 | return 27 | fi 28 | 29 | # Interactive shells - show help message. 30 | if [ -n "$PS1" ]; then 31 | show-commands 32 | fi 33 | -------------------------------------------------------------------------------- /tools/usefuncs: -------------------------------------------------------------------------------- 1 | # Helper script for setting up a project-specific environment 2 | # and commands. 3 | # 4 | # Copyright 2015 Google Inc. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # Sourced from a 'use' script containing: 19 | # 20 | # export PROJECT= 21 | # export TOOLS_DIR="$(unset CDPATH; cd $(dirname ${BASH_SOURCE[0]}) && pwd)" 22 | # source $TOOLS_DIR/usefuncs 23 | # add_alias "" 24 | # ... 25 | # show-commands 26 | # 27 | # Executables in this directory should have a header commment formated like: 28 | # 29 | # --- 30 | 31 | export PROJ_DIR="$(dirname $TOOLS_DIR)" 32 | export PATH 33 | 34 | ENV_ALIASES="" 35 | 36 | reuse () { 37 | source "$TOOLS_DIR/use" 38 | } 39 | 40 | add_alias () { 41 | a="$1"; shift 42 | d="$1"; shift 43 | ENV_ALIASES="$ENV_ALIASES $a" 44 | alias $a="$d" 45 | } 46 | 47 | unuse () { 48 | unset -f reuse 49 | unset -f add_alias 50 | unset -f unuse 51 | unset -f display-command 52 | unset -f show-commands 53 | unset -f show-dirs 54 | 55 | unset PROJECT 56 | 57 | if [[ "$_OLD_CDPATH" != "NONE" ]] ; then 58 | # Don't export CDPATH to child processes. 59 | CDPATH="$_OLD_CDPATH" 60 | else 61 | unset CDPATH 62 | fi 63 | unset _OLD_CDPATH 64 | 65 | if [ -n "$_OLD_PATH" ] ; then 66 | PATH="$_OLD_PATH" 67 | unset _OLD_PATH 68 | fi 69 | 70 | if [[ "$ENV_ALIASES" != "" ]]; then 71 | unalias $ENV_ALIASES 72 | fi 73 | unset ENV_ALIASES 74 | } 75 | 76 | display-command () { 77 | command=$1; shift 78 | description=$1; shift 79 | if [[ "$description" == "" ]]; then 80 | return 81 | fi 82 | echo "$(printf "%-20s" "$command") $description" 83 | } 84 | 85 | show-commands () { 86 | echo "Commands available in the $PROJECT project:" 87 | echo 88 | 89 | for a in $ENV_ALIASES; do 90 | HELP=$(alias $a | sed -e 's/^.*=.//' | sed -e 's/.$//') 91 | display-command "$a" "$HELP" 92 | done 93 | 94 | echo 95 | 96 | display-command reuse "Re-read the current project environment files." 97 | display-command unuse "Deactivate this project environment." 98 | display-command show-commands "Show this helpful message." 99 | 100 | echo 101 | 102 | for prog in $(find $TOOLS_DIR -maxdepth 1 -perm +111 -type f | sort); do 103 | HELP=$(grep '^#.*\-\-\-' $prog | sed 's/^.*\-\-\- //') 104 | 105 | display-command "$(basename $prog)" "$HELP" 106 | done 107 | 108 | echo 109 | echo "Project directories available by CDPATH:" 110 | 111 | show-dirs 112 | } 113 | 114 | show-dirs () { 115 | # Split CDPATH up on colons and for each item within the project directory, 116 | # show its contents as an available command 117 | ( 118 | IFS=":" 119 | for entry in ${CDPATH}; do 120 | # If this entry isn't inside $PROJ_DIR, disregard it 121 | if [[ "${entry##$PROJ_DIR}" != "${entry}" ]]; then 122 | find "${entry}" -mindepth 1 -maxdepth 1 -type d 123 | fi 124 | done 125 | ) | sed " 126 | # Convert to a literal variable reference 127 | s,$PROJ_DIR,\$PROJ_DIR,; 128 | 129 | # Prepend the last path segment to the whole command 130 | s,.*/\\(.*\\),\\1 &,; 131 | 132 | # Discard path segments that start with dot (i.e. ignore .git) 133 | /^\\./ d; 134 | " | sort -u | while read line; do 135 | command="$(echo "${line}" | sed 's/ .*//')" 136 | help="$(echo "${line}" | sed 's/[^ ]* //')" 137 | display-command "cd ${command}" "cd ${help}" 138 | done 139 | } 140 | 141 | if [ -z "$_OLD_CDPATH" ]; then 142 | if [ -n "$CDPATH" ]; then 143 | _OLD_CDPATH="$CDPATH" 144 | else 145 | _OLD_CDPATH="NONE" 146 | fi 147 | fi 148 | 149 | CDPATH=".:$PROJ_DIR" 150 | if [[ "$_OLD_CDPATH" != "NONE" ]]; then 151 | CDPATH="$CDPATH:$_OLD_CDPATH" 152 | fi 153 | 154 | if [ -z "$_OLD_PATH" ]; then 155 | _OLD_PATH="$PATH" 156 | fi 157 | PATH="$TOOLS_DIR:$_OLD_PATH" 158 | 159 | add_alias cd-home "cd \$PROJ_DIR" 160 | -------------------------------------------------------------------------------- /tools/web-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # web-server --- Manage a web server on the current directory. 3 | # 4 | # Copyright 2015 Google Inc. All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -e 19 | 20 | TOOLS_DIR="$(unset CDPATH; cd $(dirname ${BASH_SOURCE[0]}) && pwd)" 21 | 22 | VAR_DIR="$HOME/var" 23 | 24 | ALL_COMMANDS="start stop stopall restart status logs" 25 | start_help="Start a web server." 26 | stop_help="Stop a web server." 27 | stopall_help="Stop all web servers." 28 | restart_help="Stop and then restart a web server." 29 | status_help="Show information about running web servers." 30 | logs_help="Show tail lines from log file." 31 | 32 | START_PORT="8000" 33 | MAX_NAME=30 34 | PID_COLUMNS="pid,%cpu,pmem,start,time,command" 35 | 36 | usage () { 37 | local flag command service 38 | 39 | echo "Manage local web server on the current directory." 40 | usage_helper 41 | sep="[" 42 | for command in $ALL_COMMANDS; do 43 | echo -n "${sep}$command" 44 | sep="|" 45 | done 46 | echo -n "] " 47 | 48 | echo "[server_name] " 49 | 50 | show_help " --" $ALL_FLAGS 51 | echo 52 | show_help " " $ALL_COMMANDS 53 | } 54 | 55 | source $TOOLS_DIR/bash-helper 56 | 57 | web_server () { 58 | local PORT=$1; shift 59 | local LOG_FILE=$1; shift 60 | 61 | # Was: 62 | # nohup python -m SimpleHTTPServer $port 2>&1 > $VAR_DIR/logs/$service.log & 63 | # But see security issue: 64 | # https://scott.mn/2014/01/05/simplehttpserver_considered_harmful/ 65 | nohup http-server -a localhost -p $PORT 2>&1 > $LOG_FILE & 66 | } 67 | 68 | mkdir -p "$VAR_DIR/pids" 69 | mkdir -p "$VAR_DIR/logs" 70 | mkdir -p "$VAR_DIR/ports" 71 | 72 | if [ -z "$1" ]; then 73 | COMMAND=status 74 | else 75 | if is_in_list $1 $ALL_COMMANDS; then 76 | COMMAND="$1"; shift 77 | else 78 | echo "Invalid command: $1" 79 | usage 80 | exit 1 81 | fi 82 | fi 83 | 84 | rel_path () { 85 | local to="$1"; shift 86 | local from="$1"; shift 87 | 88 | python -c "import os.path; print os.path.relpath('$to', '$from')" 89 | } 90 | 91 | path_to_name () { 92 | local path="$1"; shift 93 | 94 | path="$(rel_path $path $HOME)" 95 | path="${path//\//-}" 96 | path="${path//\./_}" 97 | echo $path 98 | } 99 | 100 | if [ -n "$1" ]; then 101 | SERVERS="$1"; shift 102 | else 103 | if [[ $COMMAND == status || $COMMAND == logs || $COMMAND == stopall ]]; then 104 | SERVERS="$(ls $VAR_DIR/ports)" 105 | SERVERS=${SERVERS//.port/} 106 | else 107 | SERVERS="$(path_to_name $(pwd))" 108 | fi 109 | fi 110 | 111 | get_pid () { 112 | local pid_file="$VAR_DIR/pids/$1.pid"; shift 113 | if [[ -f $pid_file ]]; then 114 | local pid="$(cat $pid_file)" 115 | if ps -p $pid > /dev/null; then 116 | echo "$pid" 117 | return 118 | fi 119 | 120 | rm $pid_file 121 | fi 122 | } 123 | 124 | get_port () { 125 | local port_file="$VAR_DIR/ports/$1.port"; shift 126 | if [[ -f $port_file ]]; then 127 | local port="$(cat $port_file)" 128 | echo $port 129 | fi 130 | } 131 | 132 | stop_pid () { 133 | local service="$1"; shift 134 | 135 | local pidfile="$VAR_DIR/pids/$service.pid" 136 | local portfile="$VAR_DIR/ports/$service.port" 137 | echo "Stopping $service on port $(get_port $service)..." 138 | kill $(get_pid $service) 139 | rm $pidfile 140 | rm $portfile 141 | } 142 | 143 | start_webserver () { 144 | local service="$1"; shift 145 | local port="$START_PORT" 146 | local known_ports="$(cat $VAR_DIR/ports/*.port 2>/dev/null)" 147 | while true; do 148 | if is_in_list $port "$known_ports"; then 149 | port=$((port + 1)) 150 | continue 151 | fi 152 | echo -n "Starting $service on port $port..." 153 | web_server $port $VAR_DIR/logs/$service.log 154 | echo $! > $VAR_DIR/pids/$service.pid 155 | sleep 1 156 | if [ -z "$(get_pid $service)" ]; then 157 | echo "already in use..." 158 | port=$((port + 1)) 159 | else 160 | echo "connected!" 161 | break 162 | fi 163 | done 164 | echo $port > $VAR_DIR/ports/$service.port 165 | } 166 | 167 | open_browser () { 168 | local port="$1"; shift 169 | python -m webbrowser -t http://localhost:${port}/dist 170 | } 171 | 172 | # Print heading over all the ps status's echoed below. 173 | if [[ $COMMAND == "status" ]]; then 174 | status="$(ps -p 999 -o $PID_COLUMNS | head -n 1)" 175 | echo "$(printf "%-${MAX_NAME}s" "Root") $status" 176 | fi 177 | 178 | for service in $SERVERS; do 179 | pid="$(get_pid $service)" 180 | case $COMMAND in 181 | start) 182 | if [ -n "$pid" ]; then 183 | echo "$service is already started on port $(get_port $service)." 184 | else 185 | start_webserver $service 186 | fi 187 | open_browser $(get_port $service) 188 | ;; 189 | stop | stopall) 190 | if [ -z "$pid" ]; then 191 | echo "$service is not running." 192 | else 193 | stop_pid $service 194 | fi 195 | ;; 196 | restart) 197 | if [ -n "$pid" ]; then 198 | stop_pid $service 199 | fi 200 | start_webserver $service 201 | open_browser $(get_port $service) 202 | ;; 203 | status) 204 | if [ -n "$pid" ]; then 205 | status="$(ps -p $pid -o $PID_COLUMNS | tail -n +2)" 206 | else 207 | status="=== NOT RUNNING ===" 208 | fi 209 | echo "$(printf "%-${MAX_NAME}s" $service) $status" 210 | ;; 211 | logs) 212 | echo "=======================" 213 | echo "$service logs: ($VAR_DIR/logs/$service.log)" 214 | echo "=======================" 215 | tail -n 20 $VAR_DIR/logs/$service.log || true 216 | ;; 217 | esac 218 | done 219 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "rootDir": "src", 5 | "module": "commonjs", 6 | "noImplicitAny": true, 7 | "strictNullChecks": true, 8 | "preserveConstEnums": true, 9 | "inlineSourceMap": true, 10 | "noEmitOnError": true, 11 | "declaration": true 12 | }, 13 | "exclude": [ 14 | "lib", 15 | "node_modules", 16 | "typings/browser", 17 | "typings/browser.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "forin": true, 8 | "indent": [true, "spaces"], 9 | "label-position": true, 10 | "label-undefined": true, 11 | "max-line-length": [true, 140], 12 | "member-ordering": [true, 13 | "static-before-instance", 14 | "variables-before-functions" 15 | ], 16 | "no-arg": true, 17 | "no-bitwise": true, 18 | "no-console": [true, 19 | "debug", 20 | "info", 21 | "time", 22 | "timeEnd", 23 | "trace" 24 | ], 25 | "no-construct": true, 26 | "no-debugger": true, 27 | "no-duplicate-key": true, 28 | "no-duplicate-variable": true, 29 | "no-empty": true, 30 | "no-eval": true, 31 | "no-shadowed-variable": true, 32 | "no-string-literal": false, 33 | "no-switch-case-fall-through": true, 34 | "no-trailing-comma": false, 35 | "no-trailing-whitespace": true, 36 | "no-unused-expression": true, 37 | "no-unused-variable": true, 38 | "no-unreachable": true, 39 | "no-use-before-declare": true, 40 | "no-var-keyword": false, 41 | "one-line": [true, 42 | "check-open-brace", 43 | "check-catch", 44 | "check-else", 45 | "check-whitespace" 46 | ], 47 | "quotemark": false, 48 | "radix": true, 49 | "semicolon": true, 50 | "sort-object-literal-keys": false, 51 | "triple-equals": [true, "allow-null-check"], 52 | "variable-name": false, 53 | "whitespace": [true, 54 | "check-branch", 55 | "check-decl", 56 | "check-operator", 57 | "check-separator", 58 | "check-type" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase-bolt", 3 | "dependencies": {}, 4 | "devDependencies": {}, 5 | "ambientDependencies": { 6 | "chai": "github:DefinitelyTyped/DefinitelyTyped/chai/chai.d.ts#1914a00b3c740348dae407ee0d2be89b0b58ad7f", 7 | "es6-promise": "github:DefinitelyTyped/DefinitelyTyped/es6-promise/es6-promise.d.ts#830e8ebd9ef137d039d5c7ede24a421f08595f83", 8 | "mocha": "github:DefinitelyTyped/DefinitelyTyped/mocha/mocha.d.ts#d6dd320291705694ba8e1a79497a908e9f5e6617", 9 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#20e1eb9616922d382d918cc5a21870a9dbe255f5", 10 | "node-uuid": "github:DefinitelyTyped/DefinitelyTyped/node-uuid/node-uuid.d.ts#ce340e14bc4cddb77f1dd0e58faafff1e5f0c3e5" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /typings/browser.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /typings/browser/ambient/es6-promise/es6-promise.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/830e8ebd9ef137d039d5c7ede24a421f08595f83/es6-promise/es6-promise.d.ts 3 | // Type definitions for es6-promise 4 | // Project: https://github.com/jakearchibald/ES6-Promise 5 | // Definitions by: François de Campredon , vvakame 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | interface Thenable { 9 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Thenable; 10 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Thenable; 11 | catch(onRejected?: (error: any) => U | Thenable): Thenable; 12 | } 13 | 14 | declare class Promise implements Thenable { 15 | /** 16 | * If you call resolve in the body of the callback passed to the constructor, 17 | * your promise is fulfilled with result object passed to resolve. 18 | * If you call reject your promise is rejected with the object passed to reject. 19 | * For consistency and debugging (eg stack traces), obj should be an instanceof Error. 20 | * Any errors thrown in the constructor callback will be implicitly passed to reject(). 21 | */ 22 | constructor(callback: (resolve : (value?: T | Thenable) => void, reject: (error?: any) => void) => void); 23 | 24 | /** 25 | * onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects. 26 | * Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called. 27 | * Both callbacks have a single parameter , the fulfillment value or rejection reason. 28 | * "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve. 29 | * If an error is thrown in the callback, the returned promise rejects with that error. 30 | * 31 | * @param onFulfilled called when/if "promise" resolves 32 | * @param onRejected called when/if "promise" rejects 33 | */ 34 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Promise; 35 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Promise; 36 | 37 | /** 38 | * Sugar for promise.then(undefined, onRejected) 39 | * 40 | * @param onRejected called when/if "promise" rejects 41 | */ 42 | catch(onRejected?: (error: any) => U | Thenable): Promise; 43 | } 44 | 45 | declare module Promise { 46 | /** 47 | * Make a new promise from the thenable. 48 | * A thenable is promise-like in as far as it has a "then" method. 49 | */ 50 | function resolve(value?: T | Thenable): Promise; 51 | 52 | /** 53 | * Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error 54 | */ 55 | function reject(error: any): Promise; 56 | 57 | /** 58 | * Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. 59 | * the array passed to all can be a mixture of promise-like objects and other objects. 60 | * The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value. 61 | */ 62 | function all(promises: (T | Thenable)[]): Promise; 63 | 64 | /** 65 | * Make a Promise that fulfills when any item fulfills, and rejects if any item rejects. 66 | */ 67 | function race(promises: (T | Thenable)[]): Promise; 68 | } 69 | 70 | declare module 'es6-promise' { 71 | var foo: typeof Promise; // Temp variable to reference Promise in local context 72 | module rsvp { 73 | export var Promise: typeof foo; 74 | } 75 | export = rsvp; 76 | } -------------------------------------------------------------------------------- /typings/browser/ambient/mocha/mocha.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/d6dd320291705694ba8e1a79497a908e9f5e6617/mocha/mocha.d.ts 3 | // Type definitions for mocha 2.2.5 4 | // Project: http://mochajs.org/ 5 | // Definitions by: Kazi Manzur Rashid , otiai10 , jt000 , Vadim Macagon 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | interface MochaSetupOptions { 9 | //milliseconds to wait before considering a test slow 10 | slow?: number; 11 | 12 | // timeout in milliseconds 13 | timeout?: number; 14 | 15 | // ui name "bdd", "tdd", "exports" etc 16 | ui?: string; 17 | 18 | //array of accepted globals 19 | globals?: any[]; 20 | 21 | // reporter instance (function or string), defaults to `mocha.reporters.Spec` 22 | reporter?: any; 23 | 24 | // bail on the first test failure 25 | bail?: boolean; 26 | 27 | // ignore global leaks 28 | ignoreLeaks?: boolean; 29 | 30 | // grep string or regexp to filter tests with 31 | grep?: any; 32 | } 33 | 34 | interface MochaDone { 35 | (error?: Error): void; 36 | } 37 | 38 | declare var mocha: Mocha; 39 | declare var describe: Mocha.IContextDefinition; 40 | declare var xdescribe: Mocha.IContextDefinition; 41 | // alias for `describe` 42 | declare var context: Mocha.IContextDefinition; 43 | // alias for `describe` 44 | declare var suite: Mocha.IContextDefinition; 45 | declare var it: Mocha.ITestDefinition; 46 | declare var xit: Mocha.ITestDefinition; 47 | // alias for `it` 48 | declare var test: Mocha.ITestDefinition; 49 | 50 | declare function before(action: () => void): void; 51 | 52 | declare function before(action: (done: MochaDone) => void): void; 53 | 54 | declare function before(description: string, action: () => void): void; 55 | 56 | declare function before(description: string, action: (done: MochaDone) => void): void; 57 | 58 | declare function setup(action: () => void): void; 59 | 60 | declare function setup(action: (done: MochaDone) => void): void; 61 | 62 | declare function after(action: () => void): void; 63 | 64 | declare function after(action: (done: MochaDone) => void): void; 65 | 66 | declare function after(description: string, action: () => void): void; 67 | 68 | declare function after(description: string, action: (done: MochaDone) => void): void; 69 | 70 | declare function teardown(action: () => void): void; 71 | 72 | declare function teardown(action: (done: MochaDone) => void): void; 73 | 74 | declare function beforeEach(action: () => void): void; 75 | 76 | declare function beforeEach(action: (done: MochaDone) => void): void; 77 | 78 | declare function beforeEach(description: string, action: () => void): void; 79 | 80 | declare function beforeEach(description: string, action: (done: MochaDone) => void): void; 81 | 82 | declare function suiteSetup(action: () => void): void; 83 | 84 | declare function suiteSetup(action: (done: MochaDone) => void): void; 85 | 86 | declare function afterEach(action: () => void): void; 87 | 88 | declare function afterEach(action: (done: MochaDone) => void): void; 89 | 90 | declare function afterEach(description: string, action: () => void): void; 91 | 92 | declare function afterEach(description: string, action: (done: MochaDone) => void): void; 93 | 94 | declare function suiteTeardown(action: () => void): void; 95 | 96 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 97 | 98 | declare class Mocha { 99 | constructor(options?: { 100 | grep?: RegExp; 101 | ui?: string; 102 | reporter?: string; 103 | timeout?: number; 104 | bail?: boolean; 105 | }); 106 | 107 | /** Setup mocha with the given options. */ 108 | setup(options: MochaSetupOptions): Mocha; 109 | bail(value?: boolean): Mocha; 110 | addFile(file: string): Mocha; 111 | /** Sets reporter by name, defaults to "spec". */ 112 | reporter(name: string): Mocha; 113 | /** Sets reporter constructor, defaults to mocha.reporters.Spec. */ 114 | reporter(reporter: (runner: Mocha.IRunner, options: any) => any): Mocha; 115 | ui(value: string): Mocha; 116 | grep(value: string): Mocha; 117 | grep(value: RegExp): Mocha; 118 | invert(): Mocha; 119 | ignoreLeaks(value: boolean): Mocha; 120 | checkLeaks(): Mocha; 121 | /** 122 | * Function to allow assertion libraries to throw errors directly into mocha. 123 | * This is useful when running tests in a browser because window.onerror will 124 | * only receive the 'message' attribute of the Error. 125 | */ 126 | throwError(error: Error): void; 127 | /** Enables growl support. */ 128 | growl(): Mocha; 129 | globals(value: string): Mocha; 130 | globals(values: string[]): Mocha; 131 | useColors(value: boolean): Mocha; 132 | useInlineDiffs(value: boolean): Mocha; 133 | timeout(value: number): Mocha; 134 | slow(value: number): Mocha; 135 | enableTimeouts(value: boolean): Mocha; 136 | asyncOnly(value: boolean): Mocha; 137 | noHighlighting(value: boolean): Mocha; 138 | /** Runs tests and invokes `onComplete()` when finished. */ 139 | run(onComplete?: (failures: number) => void): Mocha.IRunner; 140 | } 141 | 142 | // merge the Mocha class declaration with a module 143 | declare module Mocha { 144 | /** Partial interface for Mocha's `Runnable` class. */ 145 | interface IRunnable { 146 | title: string; 147 | fn: Function; 148 | async: boolean; 149 | sync: boolean; 150 | timedOut: boolean; 151 | } 152 | 153 | /** Partial interface for Mocha's `Suite` class. */ 154 | interface ISuite { 155 | parent: ISuite; 156 | title: string; 157 | 158 | fullTitle(): string; 159 | } 160 | 161 | /** Partial interface for Mocha's `Test` class. */ 162 | interface ITest extends IRunnable { 163 | parent: ISuite; 164 | pending: boolean; 165 | 166 | fullTitle(): string; 167 | } 168 | 169 | /** Partial interface for Mocha's `Runner` class. */ 170 | interface IRunner {} 171 | 172 | interface IContextDefinition { 173 | (description: string, spec: () => void): ISuite; 174 | only(description: string, spec: () => void): ISuite; 175 | skip(description: string, spec: () => void): void; 176 | timeout(ms: number): void; 177 | } 178 | 179 | interface ITestDefinition { 180 | (expectation: string, assertion?: () => void): ITest; 181 | (expectation: string, assertion?: (done: MochaDone) => void): ITest; 182 | only(expectation: string, assertion?: () => void): ITest; 183 | only(expectation: string, assertion?: (done: MochaDone) => void): ITest; 184 | skip(expectation: string, assertion?: () => void): void; 185 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 186 | timeout(ms: number): void; 187 | } 188 | 189 | export module reporters { 190 | export class Base { 191 | stats: { 192 | suites: number; 193 | tests: number; 194 | passes: number; 195 | pending: number; 196 | failures: number; 197 | }; 198 | 199 | constructor(runner: IRunner); 200 | } 201 | 202 | export class Doc extends Base {} 203 | export class Dot extends Base {} 204 | export class HTML extends Base {} 205 | export class HTMLCov extends Base {} 206 | export class JSON extends Base {} 207 | export class JSONCov extends Base {} 208 | export class JSONStream extends Base {} 209 | export class Landing extends Base {} 210 | export class List extends Base {} 211 | export class Markdown extends Base {} 212 | export class Min extends Base {} 213 | export class Nyan extends Base {} 214 | export class Progress extends Base { 215 | /** 216 | * @param options.open String used to indicate the start of the progress bar. 217 | * @param options.complete String used to indicate a complete test on the progress bar. 218 | * @param options.incomplete String used to indicate an incomplete test on the progress bar. 219 | * @param options.close String used to indicate the end of the progress bar. 220 | */ 221 | constructor(runner: IRunner, options?: { 222 | open?: string; 223 | complete?: string; 224 | incomplete?: string; 225 | close?: string; 226 | }); 227 | } 228 | export class Spec extends Base {} 229 | export class TAP extends Base {} 230 | export class XUnit extends Base { 231 | constructor(runner: IRunner, options?: any); 232 | } 233 | } 234 | } 235 | 236 | declare module "mocha" { 237 | export = Mocha; 238 | } -------------------------------------------------------------------------------- /typings/main.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /typings/main/ambient/es6-promise/es6-promise.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/830e8ebd9ef137d039d5c7ede24a421f08595f83/es6-promise/es6-promise.d.ts 3 | // Type definitions for es6-promise 4 | // Project: https://github.com/jakearchibald/ES6-Promise 5 | // Definitions by: François de Campredon , vvakame 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | interface Thenable { 9 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Thenable; 10 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Thenable; 11 | catch(onRejected?: (error: any) => U | Thenable): Thenable; 12 | } 13 | 14 | declare class Promise implements Thenable { 15 | /** 16 | * If you call resolve in the body of the callback passed to the constructor, 17 | * your promise is fulfilled with result object passed to resolve. 18 | * If you call reject your promise is rejected with the object passed to reject. 19 | * For consistency and debugging (eg stack traces), obj should be an instanceof Error. 20 | * Any errors thrown in the constructor callback will be implicitly passed to reject(). 21 | */ 22 | constructor(callback: (resolve : (value?: T | Thenable) => void, reject: (error?: any) => void) => void); 23 | 24 | /** 25 | * onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects. 26 | * Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called. 27 | * Both callbacks have a single parameter , the fulfillment value or rejection reason. 28 | * "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve. 29 | * If an error is thrown in the callback, the returned promise rejects with that error. 30 | * 31 | * @param onFulfilled called when/if "promise" resolves 32 | * @param onRejected called when/if "promise" rejects 33 | */ 34 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Promise; 35 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Promise; 36 | 37 | /** 38 | * Sugar for promise.then(undefined, onRejected) 39 | * 40 | * @param onRejected called when/if "promise" rejects 41 | */ 42 | catch(onRejected?: (error: any) => U | Thenable): Promise; 43 | } 44 | 45 | declare module Promise { 46 | /** 47 | * Make a new promise from the thenable. 48 | * A thenable is promise-like in as far as it has a "then" method. 49 | */ 50 | function resolve(value?: T | Thenable): Promise; 51 | 52 | /** 53 | * Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error 54 | */ 55 | function reject(error: any): Promise; 56 | 57 | /** 58 | * Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. 59 | * the array passed to all can be a mixture of promise-like objects and other objects. 60 | * The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value. 61 | */ 62 | function all(promises: (T | Thenable)[]): Promise; 63 | 64 | /** 65 | * Make a Promise that fulfills when any item fulfills, and rejects if any item rejects. 66 | */ 67 | function race(promises: (T | Thenable)[]): Promise; 68 | } 69 | 70 | declare module 'es6-promise' { 71 | var foo: typeof Promise; // Temp variable to reference Promise in local context 72 | module rsvp { 73 | export var Promise: typeof foo; 74 | } 75 | export = rsvp; 76 | } -------------------------------------------------------------------------------- /typings/main/ambient/mocha/mocha.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/d6dd320291705694ba8e1a79497a908e9f5e6617/mocha/mocha.d.ts 3 | // Type definitions for mocha 2.2.5 4 | // Project: http://mochajs.org/ 5 | // Definitions by: Kazi Manzur Rashid , otiai10 , jt000 , Vadim Macagon 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | interface MochaSetupOptions { 9 | //milliseconds to wait before considering a test slow 10 | slow?: number; 11 | 12 | // timeout in milliseconds 13 | timeout?: number; 14 | 15 | // ui name "bdd", "tdd", "exports" etc 16 | ui?: string; 17 | 18 | //array of accepted globals 19 | globals?: any[]; 20 | 21 | // reporter instance (function or string), defaults to `mocha.reporters.Spec` 22 | reporter?: any; 23 | 24 | // bail on the first test failure 25 | bail?: boolean; 26 | 27 | // ignore global leaks 28 | ignoreLeaks?: boolean; 29 | 30 | // grep string or regexp to filter tests with 31 | grep?: any; 32 | } 33 | 34 | interface MochaDone { 35 | (error?: Error): void; 36 | } 37 | 38 | declare var mocha: Mocha; 39 | declare var describe: Mocha.IContextDefinition; 40 | declare var xdescribe: Mocha.IContextDefinition; 41 | // alias for `describe` 42 | declare var context: Mocha.IContextDefinition; 43 | // alias for `describe` 44 | declare var suite: Mocha.IContextDefinition; 45 | declare var it: Mocha.ITestDefinition; 46 | declare var xit: Mocha.ITestDefinition; 47 | // alias for `it` 48 | declare var test: Mocha.ITestDefinition; 49 | 50 | declare function before(action: () => void): void; 51 | 52 | declare function before(action: (done: MochaDone) => void): void; 53 | 54 | declare function before(description: string, action: () => void): void; 55 | 56 | declare function before(description: string, action: (done: MochaDone) => void): void; 57 | 58 | declare function setup(action: () => void): void; 59 | 60 | declare function setup(action: (done: MochaDone) => void): void; 61 | 62 | declare function after(action: () => void): void; 63 | 64 | declare function after(action: (done: MochaDone) => void): void; 65 | 66 | declare function after(description: string, action: () => void): void; 67 | 68 | declare function after(description: string, action: (done: MochaDone) => void): void; 69 | 70 | declare function teardown(action: () => void): void; 71 | 72 | declare function teardown(action: (done: MochaDone) => void): void; 73 | 74 | declare function beforeEach(action: () => void): void; 75 | 76 | declare function beforeEach(action: (done: MochaDone) => void): void; 77 | 78 | declare function beforeEach(description: string, action: () => void): void; 79 | 80 | declare function beforeEach(description: string, action: (done: MochaDone) => void): void; 81 | 82 | declare function suiteSetup(action: () => void): void; 83 | 84 | declare function suiteSetup(action: (done: MochaDone) => void): void; 85 | 86 | declare function afterEach(action: () => void): void; 87 | 88 | declare function afterEach(action: (done: MochaDone) => void): void; 89 | 90 | declare function afterEach(description: string, action: () => void): void; 91 | 92 | declare function afterEach(description: string, action: (done: MochaDone) => void): void; 93 | 94 | declare function suiteTeardown(action: () => void): void; 95 | 96 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 97 | 98 | declare class Mocha { 99 | constructor(options?: { 100 | grep?: RegExp; 101 | ui?: string; 102 | reporter?: string; 103 | timeout?: number; 104 | bail?: boolean; 105 | }); 106 | 107 | /** Setup mocha with the given options. */ 108 | setup(options: MochaSetupOptions): Mocha; 109 | bail(value?: boolean): Mocha; 110 | addFile(file: string): Mocha; 111 | /** Sets reporter by name, defaults to "spec". */ 112 | reporter(name: string): Mocha; 113 | /** Sets reporter constructor, defaults to mocha.reporters.Spec. */ 114 | reporter(reporter: (runner: Mocha.IRunner, options: any) => any): Mocha; 115 | ui(value: string): Mocha; 116 | grep(value: string): Mocha; 117 | grep(value: RegExp): Mocha; 118 | invert(): Mocha; 119 | ignoreLeaks(value: boolean): Mocha; 120 | checkLeaks(): Mocha; 121 | /** 122 | * Function to allow assertion libraries to throw errors directly into mocha. 123 | * This is useful when running tests in a browser because window.onerror will 124 | * only receive the 'message' attribute of the Error. 125 | */ 126 | throwError(error: Error): void; 127 | /** Enables growl support. */ 128 | growl(): Mocha; 129 | globals(value: string): Mocha; 130 | globals(values: string[]): Mocha; 131 | useColors(value: boolean): Mocha; 132 | useInlineDiffs(value: boolean): Mocha; 133 | timeout(value: number): Mocha; 134 | slow(value: number): Mocha; 135 | enableTimeouts(value: boolean): Mocha; 136 | asyncOnly(value: boolean): Mocha; 137 | noHighlighting(value: boolean): Mocha; 138 | /** Runs tests and invokes `onComplete()` when finished. */ 139 | run(onComplete?: (failures: number) => void): Mocha.IRunner; 140 | } 141 | 142 | // merge the Mocha class declaration with a module 143 | declare module Mocha { 144 | /** Partial interface for Mocha's `Runnable` class. */ 145 | interface IRunnable { 146 | title: string; 147 | fn: Function; 148 | async: boolean; 149 | sync: boolean; 150 | timedOut: boolean; 151 | } 152 | 153 | /** Partial interface for Mocha's `Suite` class. */ 154 | interface ISuite { 155 | parent: ISuite; 156 | title: string; 157 | 158 | fullTitle(): string; 159 | } 160 | 161 | /** Partial interface for Mocha's `Test` class. */ 162 | interface ITest extends IRunnable { 163 | parent: ISuite; 164 | pending: boolean; 165 | 166 | fullTitle(): string; 167 | } 168 | 169 | /** Partial interface for Mocha's `Runner` class. */ 170 | interface IRunner {} 171 | 172 | interface IContextDefinition { 173 | (description: string, spec: () => void): ISuite; 174 | only(description: string, spec: () => void): ISuite; 175 | skip(description: string, spec: () => void): void; 176 | timeout(ms: number): void; 177 | } 178 | 179 | interface ITestDefinition { 180 | (expectation: string, assertion?: () => void): ITest; 181 | (expectation: string, assertion?: (done: MochaDone) => void): ITest; 182 | only(expectation: string, assertion?: () => void): ITest; 183 | only(expectation: string, assertion?: (done: MochaDone) => void): ITest; 184 | skip(expectation: string, assertion?: () => void): void; 185 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 186 | timeout(ms: number): void; 187 | } 188 | 189 | export module reporters { 190 | export class Base { 191 | stats: { 192 | suites: number; 193 | tests: number; 194 | passes: number; 195 | pending: number; 196 | failures: number; 197 | }; 198 | 199 | constructor(runner: IRunner); 200 | } 201 | 202 | export class Doc extends Base {} 203 | export class Dot extends Base {} 204 | export class HTML extends Base {} 205 | export class HTMLCov extends Base {} 206 | export class JSON extends Base {} 207 | export class JSONCov extends Base {} 208 | export class JSONStream extends Base {} 209 | export class Landing extends Base {} 210 | export class List extends Base {} 211 | export class Markdown extends Base {} 212 | export class Min extends Base {} 213 | export class Nyan extends Base {} 214 | export class Progress extends Base { 215 | /** 216 | * @param options.open String used to indicate the start of the progress bar. 217 | * @param options.complete String used to indicate a complete test on the progress bar. 218 | * @param options.incomplete String used to indicate an incomplete test on the progress bar. 219 | * @param options.close String used to indicate the end of the progress bar. 220 | */ 221 | constructor(runner: IRunner, options?: { 222 | open?: string; 223 | complete?: string; 224 | incomplete?: string; 225 | close?: string; 226 | }); 227 | } 228 | export class Spec extends Base {} 229 | export class TAP extends Base {} 230 | export class XUnit extends Base { 231 | constructor(runner: IRunner, options?: any); 232 | } 233 | } 234 | } 235 | 236 | declare module "mocha" { 237 | export = Mocha; 238 | } --------------------------------------------------------------------------------