├── .github
└── workflows
│ ├── npm-publish.yml
│ └── npm-test.yml
├── .gitignore
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── Firestore.js
├── LICENSE
├── README.md
├── errors.js
├── fireo_logo.png
├── index.js
├── package-lock.json
├── package.json
├── src
├── fields
│ ├── BaseField.js
│ ├── BooleanField.js
│ ├── DateTimeField.js
│ ├── Field.js
│ ├── GeoPointField.js
│ ├── IDField.js
│ ├── ListField.js
│ ├── MapField.js
│ ├── NumberField.js
│ ├── ReferenceField.js
│ └── TextField.js
├── manager
│ ├── BaseManager.js
│ ├── Collection.js
│ ├── Manager.js
│ └── Query.js
└── model
│ ├── MetaModel.js
│ └── Model.js
└── test
└── v001
├── fields
├── boolean_field.test.js
├── custom_field.test.js
├── datetime_field.test.js
├── field.test.js
├── geopoint_field.test.js
├── id_field.test.js
├── list_field.test.js
├── map_field.test.js
├── number_field.test.js
├── reference_field.test.js
└── text_field.test.js
├── firestore
├── batch.test.js
├── firestore.test.js
├── firestore_operation.test.js
├── query.test.js
├── sub_collection.test.js
└── transactions.test.js
├── model
├── model.test.js
└── modelmeta.test.js
└── v1.0.up
└── limit-to-last.test.js
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v1
16 | with:
17 | node-version: 12
18 |
19 | - name: Install Firestore CLI and start emulators
20 | run: |
21 | npm i -g firebase firebase-tools
22 | firebase setup:emulators:firestore
23 | firebase emulators:start --only firestore &
24 |
25 | - name: Test Fireo Package
26 | run: |
27 | npm ci
28 | npm test
29 | env:
30 | FIRESTORE_EMULATOR_HOST: localhost:8080
31 |
32 | publish-npm:
33 | needs: build
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v2
37 | - uses: actions/setup-node@v1
38 | with:
39 | node-version: 12
40 | registry-url: https://registry.npmjs.org/
41 | - run: npm ci
42 | - run: npm publish
43 | env:
44 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
45 |
--------------------------------------------------------------------------------
/.github/workflows/npm-test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [main]
9 | pull_request:
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [12.x, 14.x, 15.x]
18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 |
27 | - name: Install Firestore CLI and start emulators
28 | run: |
29 | npm i -g firebase firebase-tools
30 | firebase setup:emulators:firestore
31 | firebase emulators:start --only firestore &
32 |
33 | - name: Test Fireo Package
34 | run: |
35 | npm ci
36 | npm test
37 | env:
38 | FIRESTORE_EMULATOR_HOST: localhost:8080
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | main.js
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "mochaExplorer.env": {
4 | "FIRESTORE_EMULATOR_HOST": "localhost:8080"
5 | }
6 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at Dev@octabyte.io. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/Firestore.js:
--------------------------------------------------------------------------------
1 | const { Firestore: CloudFirestore } = require("@google-cloud/firestore");
2 |
3 | /**
4 | * Create Firestore connection
5 | */
6 | class Firestore {
7 | /**
8 | * @constructor
9 | */
10 | constructor() {
11 | this.connection;
12 | }
13 |
14 | /**
15 | * Set firestore settings
16 | * @param {Object} settings - Firestore settings
17 | *
18 | * @param {string} [settings.projectId] - The project ID from the Google Developer's Console, e.g.
19 | * 'grape-spaceship-123'. We will also check the environment variable
20 | * GCLOUD_PROJECT for your project ID. Can be omitted in environments that
21 | * support {@link https://cloud.google.com/docs/authentication Application
22 | * Default Credentials}
23 | *
24 | * @param {string} [settings.host] - The hostname to connect to
25 | *
26 | * @param {number} [settings.port] - The port to connect to
27 | *
28 | * @param {string} [settings.keyFilename] - Local file containing the Service Account credentials as downloaded from
29 | * the Google Developers Console. Can be omitted in environments that
30 | * support {@link https://cloud.google.com/docs/authentication Application
31 | * Default Credentials}. To configure Firestore with custom credentials, use
32 | * the `credentials` property to provide the `client_email` and
33 | * `private_key` of your service account.
34 | *
35 | * @param {{client_email?: string; private_key?: string}} [settings.credentials] - The 'client_email' and 'private_key' properties of the service account
36 | * to use with your Firestore project. Can be omitted in environments that
37 | * support {@link https://cloud.google.com/docs/authentication Application
38 | * Default Credentials}. If your credentials are stored in a JSON file, you
39 | * can specify a `keyFilename` instead.
40 | *
41 | * @param {boolean} [settings.ssl] - Whether to use SSL when connecting
42 | *
43 | * @param {number} [settings.maxIdleChannels] - The maximum number of idle GRPC channels to keep. A smaller number of idle
44 | * channels reduces memory usage but increases request latency for clients
45 | * with fluctuating request rates. If set to 0, shuts down all GRPC channels
46 | * when the client becomes idle. Defaults to 1.
47 | *
48 | * @param {boolean} [settings.useBigInt] - Whether to use `BigInt` for integer types when deserializing Firestore
49 | * Documents. Regardless of magnitude, all integer values are returned as
50 | * `BigInt` to match the precision of the Firestore backend. Floating point
51 | * numbers continue to use JavaScript's `number` type.
52 | *
53 | * @param {boolean} [settings.ignoreUndefinedProperties] - Whether to skip nested properties that are set to `undefined` during
54 | * object serialization. If set to `true`, these properties are skipped
55 | * and not written to Firestore. If set `false` or omitted, the SDK throws
56 | * an exception when it encounters properties of type `undefined`.
57 | */
58 | setting(
59 | settings = {
60 | projectId,
61 | host,
62 | port,
63 | keyFilename,
64 | credentials,
65 | ssl,
66 | maxIdleChannels,
67 | useBigInt,
68 | ignoreUndefinedProperties,
69 | }
70 | ) {
71 | this.connection = new CloudFirestore(settings);
72 | }
73 |
74 | /**
75 | * Get Firestore connection
76 | */
77 | get conn() {
78 | if (!this.connection) {
79 | this.connection = new CloudFirestore();
80 | }
81 |
82 | return this.connection;
83 | }
84 | }
85 |
86 | module.exports = new Firestore();
87 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | A modern and simplest convenient ORM package in NodeJs.
5 | FireO is specifically designed for the Google's Firestore, it's more than just ORM.
6 | It implements validation, type checking, relational model logic and much more facilities.
7 |
8 |
9 |
10 | Get Started!
11 |
12 |
13 |
14 |
15 |
16 | ## Available in other language (Python)
17 |
18 | 1. FireO is available also in `python` [FireO Python](https://github.com/octabytes/FireO)
19 |
20 | ## Installation
21 |
22 | ```shell
23 | npm install fireo
24 | ```
25 |
26 | ## Example Usage
27 |
28 | ```js
29 | const {Model, Field} = require("fireo")
30 |
31 | class User extends Model:
32 | name = Field.Text();
33 |
34 |
35 | const u = User.init();
36 | u.name = "Azeem Haider";
37 | await u.save()
38 |
39 | // Get user
40 | user = await User.collection.get({key: u.key})
41 | console.log(user.name)
42 | ```
43 |
44 | ## Documentation
45 |
46 | Full documentation is available in the [FireO Doc](https://octabyte.io/fireo-nodejs/).
47 |
48 | ## Contributing
49 |
50 | Bug reports and pull requests are welcome. This project is intended to be a safe, welcoming
51 | space for collaboration, and contributors are expected to adhere to the
52 | [Contributor Covenant](https://github.com/octabytes/fireo-nodejs/blob/master/CODE_OF_CONDUCT.md) code of conduct.
53 |
54 | 1. Fix bug or add new features
55 | 2. Write tests for your functionality
56 | 3. Mention in [Documentation](https://github.com/octabytes/fireo-nodejs/tree/gh-pages), what you has done and how other can use it
57 |
58 | ## License
59 |
60 | This is official [FireO](https://github.com/octabytes/fireo-nodejs) Repository. Powered by [OctaByte](https://octabyte.io)
61 | Licensed under [Apache License 2.0](https://github.com/octabytes/fireo-nodejs/blob/master/LICENSE)
62 |
--------------------------------------------------------------------------------
/errors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * When wrong value type is added into field
3 | */
4 | class InvalidFieldType extends Error {}
5 |
6 | /**
7 | * When model is instantiate in wrong way
8 | * i.e when model is initialize from `new` keyword
9 | * instead of `Class.init()`
10 | */
11 | class InstantiateError extends Error {}
12 |
13 | /**
14 | * Required field
15 | */
16 | class RequiredField extends Error {}
17 |
18 | /**
19 | * Firestore document not found
20 | */
21 | class DocumentNotFound extends Error {}
22 |
23 | /**
24 | * Empty document can not save into firestore
25 | */
26 | class EmptyDocument extends Error {}
27 |
28 | /**
29 | * If key is not exist in Model
30 | */
31 | class KeyNotExist extends Error {}
32 |
33 | module.exports = {
34 | InvalidFieldType,
35 | InstantiateError,
36 | RequiredField,
37 | DocumentNotFound,
38 | EmptyDocument,
39 | KeyNotExist,
40 | };
41 |
--------------------------------------------------------------------------------
/fireo_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/octabytes/fireo-nodejs/163886c2a67823311f7f5dced01a4e7e08f74e40/fireo_logo.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { GeoPoint, FieldValue } = require("@google-cloud/firestore");
2 | const firestore = require("./Firestore");
3 | const Model = require("./src/model/Model");
4 | const Field = require("./src/fields/Field");
5 | const BaseField = require("./src/fields/BaseField");
6 |
7 | class Fireo {
8 | static async runTransaction(callback) {
9 | return await firestore.conn.runTransaction(async (t) => callback(t));
10 | }
11 |
12 | static batch() {
13 | return firestore.conn.batch();
14 | }
15 |
16 | /**
17 | * Create GeoPoint
18 | * @param {number} latitude The latitude as a number between -90 and 90
19 | * @param {number} longitude The longitude as a number between -180 and 180
20 | */
21 | static GeoPoint(latitude, longitude) {
22 | return new GeoPoint(latitude, longitude);
23 | }
24 |
25 | /**
26 | * Add element in firestore list
27 | * @param {Any} element - element to add in list
28 | */
29 | static listUnion(element) {
30 | return FieldValue.arrayUnion(element);
31 | }
32 |
33 | /**
34 | * Remove element in firestore list
35 | * @param {Any} element - element to remove in list
36 | */
37 | static listRemove(element) {
38 | return FieldValue.arrayRemove(element);
39 | }
40 |
41 | /**
42 | * Increment number in firestore
43 | * @param {number} number increment number
44 | */
45 | static increment(number) {
46 | return FieldValue.increment(number);
47 | }
48 |
49 | /**
50 | * Get firestore connection
51 | */
52 | static get connection() {
53 | return firestore;
54 | }
55 | }
56 |
57 | module.exports = {
58 | Fireo,
59 | Model,
60 | Field,
61 | BaseField,
62 | };
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fireo",
3 | "version": "1.0.3",
4 | "description": "Google Cloud Firestore modern and simplest convenient ORM package in NodeJs. FireO is specifically designed for the Google's Firestore",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha test/**/*test.js --timeout 15000"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/octabytes/fireo-nodejs.git"
12 | },
13 | "keywords": [
14 | "fireo",
15 | "firestore",
16 | "orm",
17 | "odm",
18 | "firestore-model",
19 | "google-cloud-firestore"
20 | ],
21 | "author": "OctaByte",
22 | "license": "Apache-2.0",
23 | "bugs": {
24 | "url": "https://github.com/octabytes/fireo-nodejs/issues"
25 | },
26 | "homepage": "https://github.com/octabytes/fireo-nodejs#readme",
27 | "devDependencies": {
28 | "chai": "^4.2.0",
29 | "chai-as-promised": "^7.1.1",
30 | "mocha": "^8.2.1"
31 | },
32 | "dependencies": {
33 | "@google-cloud/firestore": "^4.8.1"
34 | }
35 | }
--------------------------------------------------------------------------------
/src/fields/BaseField.js:
--------------------------------------------------------------------------------
1 | const { RequiredField } = require("../../errors");
2 | /**
3 | * Base class of all possible fields
4 | */
5 | class BaseField {
6 | /**
7 | * @constructor
8 | * @param {Object} options - Common options for all fields to support
9 | */
10 | constructor(
11 | options = { required: false, default: undefined, name: undefined }
12 | ) {
13 | this.val;
14 | this.modelName;
15 | this.originalName;
16 | this.toLowercase;
17 | this.required = options.required;
18 | this.default = options.default;
19 | this.customName = options.name;
20 |
21 | if (this.constructor.fieldOptions) {
22 | this.customOptions = {};
23 | for (const option of this.constructor.fieldOptions) {
24 | this.customOptions[option] = options[option];
25 | }
26 | }
27 | }
28 |
29 | /**
30 | * configure field
31 | * @param {string} modelName - Model name
32 | * @param {string} originalName - Field original name
33 | */
34 | configure(options = { modelName, originalName, toLowercase }) {
35 | this.modelName = options.modelName;
36 | this.originalName = options.originalName;
37 | this.toLowercase = options.toLowercase;
38 | }
39 |
40 | /**
41 | * Get field name if custom name is set then
42 | * return the custom name otherwise return the
43 | * original name
44 | */
45 | get name() {
46 | if (this.customName) {
47 | return this.customName;
48 | }
49 |
50 | return this.originalName;
51 | }
52 |
53 | /**
54 | * set field value
55 | * @param {any} value - value for the field
56 | */
57 | setValue(value) {
58 | this.val = value;
59 | }
60 |
61 | /**
62 | * set db field value
63 | * @param {any} value - value for the field
64 | */
65 | setDbValue(value) {
66 | this.val = value;
67 | }
68 |
69 | /**
70 | * Get value
71 | */
72 | get getValue() {
73 | let fv = this.val;
74 |
75 | // If value is not provide set the default value
76 | if (fv === undefined) {
77 | if (this.default !== undefined) {
78 | fv = this.default;
79 | }
80 | }
81 |
82 | // Check required field
83 | if (fv === undefined && this.required) {
84 | throw new RequiredField(
85 | `Field ${this.originalName} is required in Model ${this.modelName},
86 | assign value or set default for ${this.originalName} field.`
87 | );
88 | }
89 |
90 | // Check if custom options are set for this field
91 | if (this.customOptions) {
92 | for (const [option, value] of Object.entries(this.customOptions)) {
93 | fv = this["option_" + option]({
94 | optionValue: value,
95 | fieldValue: fv,
96 | });
97 | }
98 | }
99 |
100 | // Check if model config set to lowercase
101 | if (this.toLowercase) {
102 | if (this.constructor.fieldOptions.includes("toLowercase")) {
103 | fv = this["option_toLowercase"]({ optionValue: true, fieldValue: fv });
104 | }
105 | }
106 |
107 | return fv;
108 | }
109 |
110 | /**
111 | * Get DB value
112 | * Coming from Firestore and insert into model
113 | */
114 | async getDBValue() {
115 | return this.val;
116 | }
117 | }
118 |
119 | module.exports = BaseField;
120 |
--------------------------------------------------------------------------------
/src/fields/BooleanField.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("./BaseField");
2 | const { InvalidFieldType } = require("../../errors");
3 | /**
4 | * Field for Boolean data
5 | * @extends BaseField
6 | */
7 | class BooleanField extends BaseField {
8 | /**
9 | * Set BooleanField Value
10 | * @override
11 | * @param {boolean} value - boolean value
12 | */
13 | setValue(value) {
14 | if (value === undefined) {
15 | return;
16 | }
17 |
18 | if (typeof value != "boolean") {
19 | throw new InvalidFieldType(
20 | `${this.originalName} only accept boolean value in model ${this.modelName}, invalid value provided "${value}"`
21 | );
22 | }
23 |
24 | this.val = value;
25 | }
26 | }
27 |
28 | module.exports = BooleanField;
29 |
--------------------------------------------------------------------------------
/src/fields/DateTimeField.js:
--------------------------------------------------------------------------------
1 | const { FieldValue } = require("@google-cloud/firestore");
2 | const BaseField = require("./BaseField");
3 | const { InvalidFieldType } = require("../../errors");
4 | /**
5 | * Field for DateTime data
6 | * @extends BaseField
7 | */
8 | class DateTimeField extends BaseField {
9 | static fieldOptions = ["auto"];
10 |
11 | option_auto({ optionValue, fieldValue }) {
12 | if (optionValue === true) {
13 | return FieldValue.serverTimestamp();
14 | }
15 |
16 | return fieldValue;
17 | }
18 |
19 | /**
20 | * Set DateTimeField Value
21 | * @override
22 | * @param {Date} value - DateTime value
23 | */
24 | setValue(value) {
25 | if (value === undefined) {
26 | return;
27 | }
28 |
29 | if (value instanceof Date === false) {
30 | throw new InvalidFieldType(
31 | `${this.originalName} only accept Date value in model ${this.modelName}, invalid value provided "${value}"`
32 | );
33 | }
34 |
35 | this.val = value;
36 | }
37 | }
38 |
39 | module.exports = DateTimeField;
40 |
--------------------------------------------------------------------------------
/src/fields/Field.js:
--------------------------------------------------------------------------------
1 | const IDField = require("./IDField");
2 | const TextField = require("./TextField");
3 | const NumberField = require("./NumberField");
4 | const BooleanField = require("./BooleanField");
5 | const ListField = require("./ListField");
6 | const MapField = require("./MapField");
7 | const DateTimeField = require("./DateTimeField");
8 | const GeoPointField = require("./GeoPointField");
9 | const ReferenceField = require("./ReferenceField");
10 |
11 | /**
12 | * Possible Fireo Fields
13 | */
14 | class Field {
15 | static ID() {
16 | return new IDField();
17 | }
18 | static Text(
19 | options = {
20 | required: false,
21 | default: undefined,
22 | name: undefined,
23 | toLowercase: undefined,
24 | }
25 | ) {
26 | return new TextField(options);
27 | }
28 |
29 | static Number(
30 | options = { required: false, default: undefined, name: undefined }
31 | ) {
32 | return new NumberField(options);
33 | }
34 |
35 | static Boolean(
36 | options = { required: false, default: undefined, name: undefined }
37 | ) {
38 | return new BooleanField(options);
39 | }
40 |
41 | static List(
42 | options = { required: false, default: undefined, name: undefined }
43 | ) {
44 | return new ListField(options);
45 | }
46 |
47 | static Map(
48 | options = { required: false, default: undefined, name: undefined }
49 | ) {
50 | return new MapField(options);
51 | }
52 |
53 | static DateTime(
54 | options = {
55 | required: false,
56 | default: undefined,
57 | name: undefined,
58 | auto: undefined,
59 | }
60 | ) {
61 | return new DateTimeField(options);
62 | }
63 |
64 | static GeoPoint(
65 | options = { required: false, default: undefined, name: undefined }
66 | ) {
67 | return new GeoPointField(options);
68 | }
69 |
70 | static Reference(
71 | options = {
72 | required: false,
73 | default: undefined,
74 | name: undefined,
75 | autoLoad: undefined,
76 | }
77 | ) {
78 | return new ReferenceField(options);
79 | }
80 | }
81 |
82 | module.exports = Field;
83 |
--------------------------------------------------------------------------------
/src/fields/GeoPointField.js:
--------------------------------------------------------------------------------
1 | const { GeoPoint } = require("@google-cloud/firestore");
2 | const BaseField = require("./BaseField");
3 | const { InvalidFieldType } = require("../../errors");
4 | /**
5 | * Field for GeoPoint data
6 | * @extends BaseField
7 | */
8 | class GeoPointField extends BaseField {
9 | /**
10 | * Set GeoPointField Value
11 | * @override
12 | * @param {GeoPoint} value - GeoPoint value
13 | */
14 | setValue(value) {
15 | if (value === undefined) {
16 | return;
17 | }
18 |
19 | if (value instanceof GeoPoint === false) {
20 | throw new InvalidFieldType(
21 | `${this.originalName} only accept GeoPoint value in model ${this.modelName}, invalid value provided "${value}"`
22 | );
23 | }
24 |
25 | this.val = value;
26 | }
27 | }
28 |
29 | module.exports = GeoPointField;
30 |
--------------------------------------------------------------------------------
/src/fields/IDField.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Field for Firestore document id
3 | */
4 | class IDField {
5 | /**
6 | * @constructor
7 | */
8 | constructor() {
9 | this.val;
10 | this.originalName;
11 | this.modelName;
12 | }
13 |
14 | /**
15 | * configure the ID field
16 | * @param {string} originalName - Field name
17 | * @param {string} modelName - Name if the model
18 | */
19 | configure(config = { originalName, modelName }) {
20 | this.originalName = config.originalName;
21 | this.modelName = config.modelName;
22 | }
23 |
24 | /**
25 | * get field name
26 | */
27 | get name() {
28 | return this.originalName;
29 | }
30 |
31 | /**
32 | * Get value
33 | */
34 | get getValue() {
35 | return this.val;
36 | }
37 |
38 | /**
39 | * set id value
40 | * @param {string} value - custom id value
41 | */
42 | setValue(value) {
43 | this.val = value;
44 | }
45 | }
46 |
47 | module.exports = IDField;
48 |
--------------------------------------------------------------------------------
/src/fields/ListField.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("./BaseField");
2 | const { InvalidFieldType } = require("../../errors");
3 | const { FieldValue } = require("@google-cloud/firestore");
4 | /**
5 | * Field for Array data
6 | * @extends BaseField
7 | */
8 | class ListField extends BaseField {
9 | /**
10 | * Set ListField Value
11 | * @override
12 | * @param {Array} value - Array value
13 | */
14 | setValue(value) {
15 | if (value === undefined) {
16 | return;
17 | }
18 |
19 | if (!Array.isArray(value) && value instanceof FieldValue === false) {
20 | throw new InvalidFieldType(
21 | `${this.originalName} only accept Array value in model ${this.modelName}, invalid value provided "${value}"`
22 | );
23 | }
24 |
25 | this.val = value;
26 | }
27 | }
28 |
29 | module.exports = ListField;
30 |
--------------------------------------------------------------------------------
/src/fields/MapField.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("./BaseField");
2 | const { InvalidFieldType } = require("../../errors");
3 | /**
4 | * Field for Object data
5 | * @extends BaseField
6 | */
7 | class MapField extends BaseField {
8 | /**
9 | * Set MapField Value
10 | * @override
11 | * @param {Object} value - Object value
12 | */
13 | setValue(value) {
14 | if (value === undefined) {
15 | return;
16 | }
17 |
18 | if (value.constructor !== Object) {
19 | throw new InvalidFieldType(
20 | `${this.originalName} only accept Map(Object) value in model ${this.modelName}, invalid value provided "${value}"`
21 | );
22 | }
23 |
24 | this.val = value;
25 | }
26 | }
27 |
28 | module.exports = MapField;
29 |
--------------------------------------------------------------------------------
/src/fields/NumberField.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("./BaseField");
2 | const { InvalidFieldType } = require("../../errors");
3 | const { FieldValue } = require("@google-cloud/firestore");
4 | /**
5 | * Field for Number data
6 | * @extends BaseField
7 | */
8 | class NumberField extends BaseField {
9 | /**
10 | * Set NumberField Value
11 | * @override
12 | * @param {number} value - number value
13 | */
14 | setValue(value) {
15 | if (value === undefined) {
16 | return;
17 | }
18 |
19 | if (typeof value != "number" && value instanceof FieldValue === false) {
20 | throw new InvalidFieldType(
21 | `${this.originalName} only accept number value in model ${this.modelName}, invalid value provided "${value}"`
22 | );
23 | }
24 |
25 | this.val = value;
26 | }
27 | }
28 |
29 | module.exports = NumberField;
30 |
--------------------------------------------------------------------------------
/src/fields/ReferenceField.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("./BaseField");
2 | const firestore = require("../../Firestore");
3 | const { InvalidFieldType } = require("../../errors");
4 | const { DocumentReference } = require("@google-cloud/firestore");
5 |
6 | /**
7 | * Fireo document reference
8 | */
9 | class FireoDocRef {
10 | constructor(ref) {
11 | this.ref = ref;
12 | }
13 |
14 | async get() {
15 | const doc = await this.ref.get();
16 | return {
17 | id: doc.id,
18 | key: this.ref._path.segments.join("/"),
19 | ...doc.data(),
20 | };
21 | }
22 | }
23 |
24 | /**
25 | * Field for Reference doc data
26 | * @extends BaseField
27 | */
28 | class ReferenceField extends BaseField {
29 | // Custom options for this field
30 | static fieldOptions = ["autoLoad"];
31 |
32 | option_autoLoad({ optionValue, fieldValue }) {
33 | return fieldValue;
34 | }
35 |
36 | /**
37 | * Set ReferenceField Value
38 | * @override
39 | * @param {string} value - Document key
40 | */
41 | setValue(value) {
42 | if (value === undefined) {
43 | return;
44 | }
45 |
46 | if (typeof value != "string") {
47 | throw new InvalidFieldType(
48 | `${this.originalName} only accept value(string) value in model ${this.modelName}, invalid value provided "${value}"`
49 | );
50 | }
51 |
52 | this.val = firestore.conn.doc(value);
53 | }
54 |
55 | /**
56 | * Set db field value
57 | * @param {DocumentReference} value Document reference
58 | */
59 | setDbValue(value) {
60 | this.val = new FireoDocRef(value);
61 | }
62 |
63 | /**
64 | * Modify coming value from firestore
65 | * @override
66 | */
67 | async getDBValue() {
68 | if (this.customOptions.autoLoad) {
69 | return await this.val.get();
70 | }
71 |
72 | return this.val;
73 | }
74 | }
75 |
76 | module.exports = ReferenceField;
77 |
--------------------------------------------------------------------------------
/src/fields/TextField.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("./BaseField");
2 | const { InvalidFieldType } = require("../../errors");
3 | /**
4 | * Field for Text data
5 | * @extends BaseField
6 | */
7 | class TextField extends BaseField {
8 | // Custom options for this field
9 | static fieldOptions = ["toLowercase"];
10 |
11 | option_toLowercase({ optionValue, fieldValue }) {
12 | return optionValue ? fieldValue.toLowerCase() : fieldValue;
13 | }
14 |
15 | /**
16 | * Set TextField Value
17 | * @override
18 | * @param {string} value - string value
19 | */
20 | setValue(value) {
21 | if (value === undefined) {
22 | return;
23 | }
24 |
25 | if (typeof value != "string") {
26 | throw new InvalidFieldType(
27 | `${this.originalName} only accept string value in model ${this.modelName}, invalid value provided "${value}"`
28 | );
29 | }
30 |
31 | this.val = value;
32 | }
33 | }
34 |
35 | module.exports = TextField;
36 |
--------------------------------------------------------------------------------
/src/manager/BaseManager.js:
--------------------------------------------------------------------------------
1 | const firestore = require("../../Firestore");
2 |
3 | class BaseManager {
4 | /**
5 | * @constructor
6 | * @param {MetaModel.__meta} meta - Meta data of model
7 | */
8 | constructor(meta) {
9 | this.__meta = meta;
10 | }
11 |
12 | /**
13 | * Get Firestore document reference
14 | */
15 | get __getDocRef() {
16 | let collectionPath = this.__meta.collectionName;
17 |
18 | if (this.__meta.parent) {
19 | collectionPath = this.__meta.parent;
20 | collectionPath += "/" + this.__meta.collectionName;
21 | }
22 |
23 | if (this.__meta.id && this.__meta.id.getValue) {
24 | return firestore.conn
25 | .collection(collectionPath)
26 | .doc(this.__meta.id.getValue);
27 | }
28 |
29 | return firestore.conn.collection(collectionPath).doc();
30 | }
31 |
32 | /**
33 | * Generate firestore doc ref from id or key
34 | * @param {Object} by - Document id or key
35 | * @param {string} by.id - Document id
36 | * @param {string} by.key - Document key
37 | */
38 | __createDocRef(by = { id, key }) {
39 | if (by.id) {
40 | return firestore.conn.collection(this.__meta.collectionName).doc(by.id);
41 | } else {
42 | return firestore.conn.doc(by.key);
43 | }
44 | }
45 |
46 | /**
47 | * Get key from doc ref
48 | * @param {DocumentReference} docRef - Firestore document reference
49 | */
50 | __extractKeyFromDocRef(docRef) {
51 | return docRef._path.segments.join("/");
52 | }
53 | }
54 |
55 | module.exports = BaseManager;
56 |
--------------------------------------------------------------------------------
/src/manager/Collection.js:
--------------------------------------------------------------------------------
1 | const { DocumentNotFound } = require("../../errors");
2 | const BaseManager = require("./BaseManager");
3 | const Query = require("./Query");
4 | const firestore = require("../../Firestore");
5 |
6 | /**
7 | * Operate firestore static operations i.e get, query etc
8 | * @extends BaseManager
9 | */
10 | class Collection extends BaseManager {
11 | /**
12 | * Collection constructor
13 | * @override
14 | * @constructor
15 | * @param {Model} modelObj - Model object
16 | */
17 | constructor(modelObj) {
18 | super(modelObj.__meta);
19 | this.__modelObj = modelObj;
20 | }
21 |
22 | /**
23 | * Get firestore document
24 | * @param {Object} options - Document id, key and transaction
25 | * @param {string} options.id - Document id
26 | * @param {string} options.key - Document key
27 | */
28 | async get({ id, key, transaction } = {}) {
29 | const docRef = this.__createDocRef({ id, key });
30 |
31 | let snapshot;
32 | if (transaction) {
33 | snapshot = await transaction.get(docRef);
34 | } else {
35 | snapshot = await docRef.get();
36 | }
37 |
38 | if (!snapshot.exists) {
39 | throw new DocumentNotFound(
40 | `Document not found in collection ${
41 | this.__meta.collectionName
42 | } against ${id || key}`
43 | );
44 | }
45 |
46 | await this.__modelObj.__setFieldsValue({
47 | data: snapshot.data(),
48 | id: snapshot.id,
49 | key: this.__extractKeyFromDocRef(docRef),
50 | });
51 |
52 | return this.__modelObj;
53 | }
54 |
55 | /**
56 | * Delete firestore document
57 | * @param {Object} by - Document id or key
58 | * @param {string} by.id - Document id
59 | * @param {string} by.key - Document key
60 | * @param {boolean} by.child - Delete also child documents
61 | */
62 | async delete(by = { id: undefined, key: undefined, child: undefined }) {
63 | if (by.id || by.key) {
64 | const ref = this.__createDocRef(by);
65 |
66 | if (by.child) {
67 | await this.__deleteCollectionDocs([ref], true);
68 | } else {
69 | await ref.delete();
70 | }
71 | } else {
72 | const ref = firestore.conn.collection(this.__meta.collectionName);
73 | const docs = await ref.listDocuments();
74 | await this.__deleteCollectionDocs(docs, by.child);
75 | }
76 | }
77 |
78 | /**
79 | * Delete collection docs
80 | * @param {Array} docList - List of doc ref
81 | * @param {boolean} child - Delete nested child collection docs
82 | */
83 | async __deleteCollectionDocs(docList, child) {
84 | let batchSize = 0;
85 | const batch = firestore.conn.batch();
86 | for (let doc of docList) {
87 | if (child) {
88 | const docCollections = await doc.listCollections();
89 | if (docCollections.length) {
90 | for (let collection of docCollections) {
91 | await this.__deleteCollectionDocs(
92 | await collection.listDocuments(),
93 | child
94 | );
95 | }
96 | }
97 | }
98 |
99 | batch.delete(doc);
100 | batchSize++;
101 | if (batchSize >= 300) {
102 | await batch.commit();
103 | await this.__deleteCollectionDocs(docList.slice(batchSize), child);
104 | return;
105 | }
106 | }
107 |
108 | await batch.commit();
109 | }
110 |
111 | /**
112 | * Filter firestore document
113 | * @param {string} field - Name of the field
114 | * @param {string} operator - Firestore operator
115 | * @param {string} value - value to search
116 | */
117 | where(field, operator, value) {
118 | const query = new Query(this);
119 | return query.where(field, operator, value);
120 | }
121 |
122 | /**
123 | * Fetch collection docs
124 | * @param {number} limit - Limit the firestore documents
125 | */
126 | async fetch(limit) {
127 | const query = new Query(this);
128 | return await query.fetch(limit);
129 | }
130 |
131 | /**
132 | * Limit collection docs
133 | * @param {number} limit - Limit the firestore documents
134 | */
135 | limit(number) {
136 | const query = new Query(this);
137 | return query.limit(number);
138 | }
139 |
140 | /**
141 | * Limit collection docs in reverse order
142 | * @param {number} limit - Limit the firestore documents
143 | */
144 | limitToLast(number) {
145 | const query = new Query(this);
146 | return query.limitToLast(number);
147 | }
148 |
149 | /**
150 | * Order the document
151 | * @param {string} field - field name
152 | */
153 | orderBy(field) {
154 | const query = new Query(this);
155 | return query.orderBy(field);
156 | }
157 |
158 | /**
159 | * Create query from cursor
160 | * @param {string} queryCursor - Query cursor
161 | */
162 | cursor(queryCursor) {
163 | const query = new Query(this);
164 | return query.cursor(queryCursor);
165 | }
166 |
167 | /**
168 | * Set firestore offset
169 | * @param {number} number - Starting number
170 | */
171 | offset(number) {
172 | const query = new Query(this);
173 | return query.offset(number);
174 | }
175 |
176 | /**
177 | * Set parent key
178 | * @param {string} key - Key of parent document
179 | */
180 | parent(key) {
181 | const query = new Query(this);
182 | return query.parent(key);
183 | }
184 |
185 | /**
186 | * Transaction
187 | * @param {Transaction} t - Firestore transaction
188 | */
189 | transaction(t) {
190 | const query = new Query(this);
191 | return query.transaction(t);
192 | }
193 | }
194 |
195 | module.exports = Collection;
196 |
--------------------------------------------------------------------------------
/src/manager/Manager.js:
--------------------------------------------------------------------------------
1 | const { Transaction } = require("@google-cloud/firestore");
2 | const { DocumentNotFound } = require("../../errors");
3 | const BaseManager = require("./BaseManager");
4 | /**
5 | * Operate Firestore operations
6 | * @extends BaseManager
7 | */
8 | class Manager extends BaseManager {
9 | /**
10 | * Save model into firestore document
11 | * @param {Object} options - Save options
12 | * @param {boolean} options.merge - Merge the fields with existing document or create
13 | * @param {Transaction} options.transaction - Firestore transaction
14 | * new document if it already not exist
15 | */
16 | async save(options = { merge: false, transaction }) {
17 | const docRef = this.__getDocRef;
18 | if (options.transaction) {
19 | await options.transaction.set(docRef, this.__meta.parseFields, {
20 | merge: options.merge,
21 | });
22 | } else {
23 | await docRef.set(this.__meta.parseFields, { merge: options.merge });
24 | }
25 | return { id: docRef.id, key: this.__extractKeyFromDocRef(docRef) };
26 | }
27 |
28 | /**
29 | * Update model into firestore document
30 | * @param {Object} options - Update doc options
31 | * @param {string} options.id - document id
32 | * @param {string} options.key - document key
33 | * @param {Transaction} options.transaction - firestore transaction
34 | */
35 | async update({ id, key, transaction } = {}) {
36 | const docRef = this.__createDocRef({ id, key });
37 | try {
38 | if (transaction) {
39 | await transaction.update(docRef, this.__meta.parseFields);
40 | } else {
41 | await docRef.update(this.__meta.parseFields);
42 | }
43 | } catch (e) {
44 | throw new DocumentNotFound(
45 | `No document to update in collection ${
46 | this.__meta.collectionName
47 | } against ${id || key}`
48 | );
49 | }
50 | return { id: docRef.id, key: this.__extractKeyFromDocRef(docRef) };
51 | }
52 |
53 | /**
54 | * Delete document from firestore
55 | * @param {Object} options - Delete options
56 | * @param {string} options.key - Document key
57 | * @param {Transaction} options.transaction - Firestore transaction
58 | */
59 | async delete({ key, transaction } = {}) {
60 | const docRef = this.__createDocRef({ key: key });
61 | if (transaction) {
62 | await transaction.delete(docRef);
63 | } else {
64 | await docRef.delete();
65 | }
66 | }
67 | }
68 |
69 | module.exports = Manager;
70 |
--------------------------------------------------------------------------------
/src/manager/Query.js:
--------------------------------------------------------------------------------
1 | const firestore = require("../../Firestore");
2 | /**
3 | * Operate firestore query operator
4 | */
5 | class Query {
6 | /**
7 | * @constructor
8 | * @param {Collection} collection - Collection object
9 | */
10 | constructor(collection) {
11 | this.__collection = collection;
12 | this.__queryParameters = {};
13 | }
14 |
15 | /**
16 | * Get Field DB
17 | * @param {string} name - Name of the field
18 | */
19 | __getField(name) {
20 | return this.__collection.__meta.fields[name];
21 | }
22 |
23 | /**
24 | * Get Field DB name
25 | * @param {string} name - Name of the field
26 | */
27 | __getFieldName(name) {
28 | return this.__getField(name).name;
29 | }
30 |
31 | /**
32 | * Filter firestore document
33 | * @param {string} field - Name of the field
34 | * @param {string} operator - Firestore operator
35 | * @param {string} value - value to search
36 | */
37 | where(field, operator, value) {
38 | const fieldName = this.__getFieldName(field);
39 | const f = this.__getField(field);
40 | f.setValue(value);
41 |
42 | let v = f.getValue;
43 |
44 | if (this.__collection.__meta.config.toLowercase) {
45 | try {
46 | v = value.toLowerCase();
47 | } catch (e) {}
48 | }
49 |
50 | if (this.__queryParameters.filters) {
51 | this.__queryParameters.filters.push({
52 | name: fieldName,
53 | operator,
54 | value: v,
55 | });
56 | } else {
57 | this.__queryParameters.filters = [
58 | { name: fieldName, operator, value: v },
59 | ];
60 | }
61 |
62 | return this;
63 | }
64 |
65 | /**
66 | * Limit firestore
67 | * @param {number} number - Limit number of results
68 | */
69 | limit(number) {
70 | this.__queryParameters.limit = number;
71 | return this;
72 | }
73 |
74 | /**
75 | * Limit To Last, limit firestore docs in reverse order
76 | * @param {number} number - Limit number of results
77 | */
78 | limitToLast(number) {
79 | this.__queryParameters.limitToLast = number;
80 | return this;
81 | }
82 |
83 | /**
84 | * Order the document
85 | * @param {string} field - field name
86 | */
87 | orderBy(field) {
88 | if (field[0] === "-") {
89 | this.__addDocOrder(this.__getFieldName(field.slice(1)), "desc");
90 | } else {
91 | this.__addDocOrder(this.__getFieldName(field, "asc"));
92 | }
93 |
94 | return this;
95 | }
96 | __addDocOrder(field, order) {
97 | if (this.__queryParameters.order) {
98 | this.__queryParameters.order.push({ name: field, order: order });
99 | } else {
100 | this.__queryParameters.order = [{ name: field, order: order }];
101 | }
102 | }
103 |
104 | /**
105 | * Set firestore offset
106 | * @param {number} number - Starting number
107 | */
108 | offset(number) {
109 | this.__queryParameters.offset = number;
110 | return this;
111 | }
112 |
113 | /**
114 | * Create query from cursor
115 | * @param {string} queryCursor - Query cursor
116 | */
117 | cursor(queryCursor) {
118 | const c = this.__decodeCursor(queryCursor);
119 | this.__queryParameters = JSON.parse(c);
120 | return this;
121 | }
122 |
123 | /**
124 | * Set parent key
125 | * @param {string} key - Key of parent document
126 | */
127 | parent(key) {
128 | this.__queryParameters.parent = key;
129 | return this;
130 | }
131 |
132 | /**
133 | * Transaction
134 | * @param {Transaction} t - Firestore transaction
135 | */
136 | transaction(t) {
137 | this.__transaction = t;
138 | return this;
139 | }
140 |
141 | /**
142 | * Delete documents by query
143 | * @param {boolean} child - Delete child docs and collections
144 | */
145 | async delete(child) {
146 | const result = await this.fetch();
147 | const docRefs = [];
148 | for (let doc of result.list) {
149 | docRefs.push(doc.__meta.extra.ref);
150 | }
151 |
152 | await this.__collection.__deleteCollectionDocs(docRefs, child);
153 | }
154 |
155 | /**
156 | * Retrieve firestore document
157 | * @param {number} limit - Limit the number of firestore documents
158 | */
159 | async fetch(limit) {
160 | let ref;
161 |
162 | if (this.__queryParameters.parent) {
163 | ref = firestore.conn.collection(
164 | this.__queryParameters.parent +
165 | "/" +
166 | this.__collection.__meta.collectionName
167 | );
168 | } else {
169 | ref = firestore.conn.collection(this.__collection.__meta.collectionName);
170 | }
171 |
172 | if (this.__queryParameters.filters) {
173 | for (const filter of this.__queryParameters.filters) {
174 | ref = ref.where(filter.name, filter.operator, filter.value);
175 | }
176 | }
177 |
178 | if (this.__queryParameters.limit) {
179 | ref = ref.limit(this.__queryParameters.limit);
180 | }
181 |
182 | if (this.__queryParameters.limitToLast) {
183 | ref = ref.limitToLast(this.__queryParameters.limitToLast);
184 | }
185 |
186 | if (limit) {
187 | this.__queryParameters.limit = limit;
188 | ref = ref.limit(limit);
189 | }
190 |
191 | if (this.__queryParameters.order) {
192 | for (const order of this.__queryParameters.order) {
193 | ref = ref.orderBy(order.name, order.order);
194 | }
195 | }
196 |
197 | if (this.__queryParameters.offset) {
198 | ref = ref.offset(this.__queryParameters.offset);
199 | }
200 | let snapshot;
201 | if (this.__transaction) {
202 | snapshot = await this.__transaction.get(ref);
203 | } else {
204 | snapshot = await ref.get();
205 | }
206 |
207 | const modelList = [];
208 |
209 | for (let doc of snapshot.docs) {
210 | const model = this.__collection.__modelObj.constructor.init();
211 | await model.__setFieldsValue({
212 | data: doc.data(),
213 | id: doc.id,
214 | key: this.__collection.__extractKeyFromDocRef(doc._ref),
215 | extra: {
216 | ref: doc._ref,
217 | },
218 | });
219 | modelList.push(model);
220 | }
221 |
222 | return {
223 | cursor: this.__encodeCursor(JSON.stringify(this.__queryCursor)),
224 | list: modelList,
225 | };
226 | }
227 |
228 | get __queryCursor() {
229 | const c = this.__queryParameters;
230 | if (this.__queryParameters.offset) {
231 | c.offset = this.__queryParameters.offset + this.__queryParameters.limit;
232 | } else {
233 | c.offset = this.__queryParameters.limit;
234 | }
235 |
236 | return c;
237 | }
238 |
239 | __encodeCursor(cursor) {
240 | const buffer = Buffer.from(cursor, "utf8");
241 | return buffer.toString("base64");
242 | }
243 |
244 | __decodeCursor(cursor) {
245 | const buffer = Buffer.from(cursor, "base64");
246 | return buffer.toString("utf8");
247 | }
248 |
249 | /**
250 | * Get query first document
251 | */
252 | async get() {
253 | return (await this.fetch(1)).list[0];
254 | }
255 | }
256 |
257 | module.exports = Query;
258 |
--------------------------------------------------------------------------------
/src/model/MetaModel.js:
--------------------------------------------------------------------------------
1 | const BaseField = require("../fields/BaseField");
2 | const IDField = require("../fields/IDField");
3 |
4 | /**
5 | * Meta class for model
6 | */
7 | class MetaModel {
8 | constructor() {
9 | this.__meta = {};
10 | }
11 |
12 | /**
13 | * Configure the Model
14 | * @param {Object} config - Model config
15 | * @param {string} config.parent - Parent document key
16 | */
17 | __configure(config = { parent: undefined }) {
18 | this.__meta.config = this.constructor.config || {};
19 | this.__meta.parent = config.parent;
20 | this.__meta.isInstantiate = true;
21 | this.__meta.modelName = this.constructor.name;
22 | this.__getFields();
23 | this.__setCollectionName();
24 | }
25 |
26 | __setCollectionName() {
27 | this.__meta.collectionName =
28 | this.__meta.config.collectionName || this.constructor.name;
29 | }
30 |
31 | /**
32 | * Get Model properties
33 | */
34 | get __getModelProperties() {
35 | const allProperties = Object.getOwnPropertyNames(this);
36 | return allProperties.filter((f) => f != "__meta");
37 | }
38 |
39 | /**
40 | * Get Model fields
41 | */
42 | __getFields() {
43 | this.__meta.fields = {};
44 |
45 | const properties = this.__getModelProperties;
46 | for (const propertyName of properties) {
47 | const field = this[propertyName];
48 |
49 | if (field instanceof IDField) {
50 | field.configure({
51 | modelName: this.constructor.name,
52 | originalName: propertyName,
53 | });
54 | this.__meta.id = field;
55 | this[propertyName] = undefined;
56 | continue;
57 | }
58 |
59 | if (field instanceof BaseField) {
60 | field.configure({
61 | modelName: this.constructor.name,
62 | originalName: propertyName,
63 | toLowercase: this.__meta.config.toLowercase,
64 | });
65 | this.__meta.fields[propertyName] = field;
66 | this[propertyName] = undefined;
67 | }
68 | }
69 | }
70 |
71 | /**
72 | * parse field
73 | */
74 | __parseField() {
75 | this.__meta.parseFields = {};
76 |
77 | for (const [propertyName, field] of Object.entries(this.__meta.fields)) {
78 | const value = this[propertyName];
79 | if (value !== undefined) {
80 | field.setValue(this[propertyName]);
81 | this.__meta.parseFields[field.name] = field.getValue;
82 | }
83 | }
84 |
85 | // Set value of id field if any custom id provided
86 | if (this.__meta.id) {
87 | this.__meta.id.setValue(this[this.__meta.id.name]);
88 | }
89 | }
90 |
91 | /**
92 | * Set document id and key to this model object
93 | * @param {string} id - document id
94 | * @param {string} key - document key
95 | */
96 | __setIdAndKey(id, key) {
97 | this.key = key;
98 |
99 | if (this.__meta.id) {
100 | this[this.__meta.id.name] = id;
101 | } else {
102 | this.id = id;
103 | }
104 | }
105 |
106 | /**
107 | * Set values to model object
108 | * @param {Object} result - Document result
109 | * @param {Object} result.data - Document data
110 | * @param {string} result.id - Document ID
111 | * @param {string} result.key - Document key
112 | * @param {Any} result.extra - Any extra data which you want to add with model meta
113 | */
114 | async __setFieldsValue(result = { data, id, key, extra }) {
115 | for (const [propertyName, field] of Object.entries(this.__meta.fields)) {
116 | field.setDbValue(result.data[field.name]);
117 | this[propertyName] = await field.getDBValue();
118 | }
119 |
120 | this.__setIdAndKey(result.id, result.key);
121 |
122 | if (result.extra) {
123 | this.__meta.extra = result.extra;
124 | }
125 | }
126 | }
127 |
128 | module.exports = MetaModel;
129 |
--------------------------------------------------------------------------------
/src/model/Model.js:
--------------------------------------------------------------------------------
1 | const {
2 | InstantiateError,
3 | EmptyDocument,
4 | KeyNotExist,
5 | } = require("../../errors");
6 | const Collection = require("../manager/Collection");
7 | const Manager = require("../manager/Manager");
8 | const MetaModel = require("./MetaModel");
9 | /**
10 | * Firestore document model
11 | * @extends MetaModel
12 | */
13 | class Model extends MetaModel {
14 | /**
15 | * @constructor
16 | * @param {boolean} isInstantiate - Object is instantiate from init() method or not
17 | */
18 | constructor(isInstantiate = false) {
19 | super();
20 |
21 | if (!isInstantiate) {
22 | throw new InstantiateError(
23 | `Model ${this.constructor.name} should be instantiate from init() method,
24 | User ${this.constructor.name}.init() instead of new ${this.constructor.name}`
25 | );
26 | }
27 | }
28 |
29 | /**
30 | * Create model from parent key
31 | * @param {string} key - Parent document key
32 | */
33 | static parent(key) {
34 | const obj = new this(true);
35 | obj.__configure({ parent: key });
36 | return obj;
37 | }
38 |
39 | /**
40 | * Instantiate Model
41 | * @param {Object} config - Model config
42 | * @param {string} config.parent - Parent document key
43 | */
44 | static init(config = { parent: undefined }) {
45 | const obj = new this(true);
46 | obj.__configure(config);
47 | return obj;
48 | }
49 |
50 | /**
51 | * Static Create model instance from key value object
52 | * @param {Object} map - Key value pair of model fields
53 | */
54 | static fromObject(map) {
55 | const obj = new this(true);
56 | obj.__configure();
57 | const properties = obj.__getModelProperties;
58 | for (const propertyName of properties) {
59 | obj[propertyName] = map[propertyName];
60 | }
61 | return obj;
62 | }
63 |
64 | /**
65 | * Create model instance from key value object
66 | * @param {Object} map - Key value pair of model fields
67 | */
68 | fromObject(map) {
69 | const properties = this.__getModelProperties;
70 | for (const propertyName of properties) {
71 | this[propertyName] = map[propertyName];
72 | }
73 | return this;
74 | }
75 |
76 | /**
77 | * Convert Model object into key value pair object
78 | */
79 | toObject() {
80 | const map = {};
81 | const properties = this.__getModelProperties;
82 | for (const propertyName of properties) {
83 | map[propertyName] = this[propertyName];
84 | }
85 |
86 | return map;
87 | }
88 |
89 | /**
90 | * Save model into firestore document
91 | * @param {Object} options - Options for save docu
92 | * @param {boolean} options.merge - Merge the fields with existing document or create
93 | * @param {Transaction} options.transaction - Firestore transaction
94 | * @param {Batch} options.batch - Firestore batch
95 | * new document if it already not exist
96 | */
97 | async save({ merge = false, transaction, batch } = {}) {
98 | this.__parseField();
99 |
100 | // Check if document id not empty
101 | if (Object.keys(this.__meta.parseFields).length === 0) {
102 | throw new EmptyDocument(
103 | `Trying to save empty document in firestore from ${this.constructor.name}`
104 | );
105 | }
106 |
107 | const manager = new Manager(this.__meta);
108 | const transOrBatch = transaction || batch;
109 | const { id, key } = await manager.save({
110 | merge,
111 | transaction: transOrBatch,
112 | });
113 | this.__setIdAndKey(id, key);
114 | }
115 |
116 | /**
117 | * Merge the fields with existing document or create
118 | * new document if it already not exist
119 | * @param {Object} options - Options
120 | * @param {Transaction} options.transaction - Firestore transaction
121 | * @param {Batch} options.batch - Firestore batch
122 | */
123 | async upsert({ transaction, batch } = {}) {
124 | await this.save({ merge: true, transaction, batch });
125 | }
126 |
127 | /**
128 | * Update existing firestore document
129 | * @param {Object} options - Update options
130 | * @param {string} options.id - document id
131 | * @param {string} options.key - document key
132 | * @param {Transaction} options.transaction - Firestore transaction
133 | * @param {Batch} options.batch - Firestore batch
134 | */
135 | async update({ id = undefined, key = undefined, transaction, batch } = {}) {
136 | let result;
137 | this.__parseField();
138 | const manager = new Manager(this.__meta);
139 |
140 | if (!id && !key) {
141 | if (!this.key) {
142 | throw new KeyNotExist(
143 | `No key exist in Model ${this.constructor.name}, provide id or key in update() method`
144 | );
145 | }
146 |
147 | result = await manager.update({ key: this.key });
148 | } else {
149 | const transOrBatch = transaction || batch;
150 | result = await manager.update({ id, key, transaction: transOrBatch });
151 | }
152 | this.__setIdAndKey(result.id, result.key);
153 | }
154 |
155 | /**
156 | * Delete document from firestore
157 | * @param {Object} options - Delete options
158 | * @param {Transaction} options.transaction - Firestore transaction
159 | * @param {Batch} options.batch - Firestore batch
160 | */
161 | async delete({ transaction, batch } = {}) {
162 | if (!this.key) {
163 | throw new KeyNotExist(`No key exist in Model ${this.constructor.name}`);
164 | }
165 |
166 | const manager = new Manager(this.__meta);
167 | const transOrBatch = transaction || batch;
168 | await manager.delete({ key: this.key, transaction: transOrBatch });
169 | }
170 |
171 | /**
172 | * Perform firestore static operations which does not need
173 | * of model object i.e get, query etc
174 | */
175 | static get collection() {
176 | const obj = this.init();
177 | return new Collection(obj);
178 | }
179 | }
180 |
181 | module.exports = Model;
182 |
--------------------------------------------------------------------------------
/test/v001/fields/boolean_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const { InvalidFieldType, RequiredField } = require("../../../errors");
4 |
5 | const expect = chai.expect;
6 |
7 | describe("BooleanField", () => {
8 | describe("BooleanField() set value", () => {
9 | let booleanField;
10 | beforeEach(() => {
11 | booleanField = Field.Boolean();
12 | });
13 | it("can be set the value", () => {
14 | booleanField.setValue(true);
15 | expect(booleanField.getValue).to.be.true;
16 | });
17 |
18 | it("should only accept the Boolean", () => {
19 | expect(() => booleanField.setValue(true)).to.not.throw();
20 | });
21 |
22 | it("should not accept other value which are not Boolean", () => {
23 | expect(() => booleanField.setValue(0)).to.throw(InvalidFieldType);
24 | });
25 | });
26 |
27 | describe("BooleanField supported options", () => {
28 | it("should not throw error if required and value set", () => {
29 | const booleanField = Field.Boolean({ required: true });
30 | booleanField.setValue(true);
31 | expect(() => booleanField.getValue).to.not.throw();
32 | });
33 | it("should throw error if required and no value set", () => {
34 | const booleanField = Field.Boolean({ required: true });
35 | expect(() => booleanField.getValue).to.throw(RequiredField);
36 | });
37 | it("name can be change", () => {
38 | const booleanField = Field.Boolean({ name: "custom_name" });
39 | expect(booleanField.name).to.equal("custom_name");
40 | });
41 | it("has a default value", () => {
42 | const booleanField = Field.Boolean({ default: false });
43 | expect(booleanField.getValue).to.equal(false);
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/test/v001/fields/custom_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const BaseField = require("../../../src/fields/BaseField");
4 | const { Fireo } = require("../../../index");
5 |
6 | const expect = chai.expect;
7 |
8 | describe("CustomField", () => {
9 | before(() => {
10 | Fireo.connection.setting({ projectId: "fs-test-project" });
11 | });
12 | it("should modify Database value", async () => {
13 | class WeekDays extends BaseField {
14 | days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
15 |
16 | setValue(value) {
17 | this.val = this.days[value];
18 | }
19 | }
20 |
21 | class User extends Model {
22 | day = new WeekDays();
23 | }
24 |
25 | const user = User.init();
26 | user.day = 0;
27 | await user.save();
28 |
29 | const doc = await User.collection.get({ key: user.key });
30 | expect(doc.day).to.equal("Mon");
31 | });
32 |
33 | it("should able to modify value which is coming from Firestore", async () => {
34 | class WeekDays extends BaseField {
35 | days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
36 |
37 | setValue(value) {
38 | this.val = this.days[value];
39 | }
40 |
41 | async getDBValue() {
42 | return this.val + "-mod";
43 | }
44 | }
45 |
46 | class User extends Model {
47 | day = new WeekDays();
48 | }
49 |
50 | const user = User.init();
51 | user.day = 0;
52 | await user.save();
53 |
54 | const doc = await User.collection.get({ key: user.key });
55 | expect(doc.day).to.equal("Mon-mod");
56 | });
57 |
58 | it("should able to support custom options", async () => {
59 | class EmailGenerator extends BaseField {
60 | static fieldOptions = ["prefix", "domain"];
61 |
62 | option_prefix({ optionValue, fieldValue }) {
63 | return optionValue + "." + fieldValue;
64 | }
65 |
66 | option_domain({ optionValue, fieldValue }) {
67 | return fieldValue + "@" + optionValue;
68 | }
69 | }
70 |
71 | class User extends Model {
72 | email = new EmailGenerator({ prefix: "prefix", domain: "example.com" });
73 | }
74 |
75 | const user = User.init();
76 | user.email = "my_email";
77 | await user.save();
78 |
79 | const doc = await User.collection.get({ key: user.key });
80 | expect(doc.email).to.equal("prefix.my_email@example.com");
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/v001/fields/datetime_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const { InvalidFieldType, RequiredField } = require("../../../errors");
4 |
5 | const expect = chai.expect;
6 |
7 | describe("DateTime", () => {
8 | describe("dateTime() set value", () => {
9 | let dateTime;
10 | beforeEach(() => {
11 | dateTime = Field.DateTime();
12 | });
13 | it("can be set the value", () => {
14 | const dt = new Date();
15 | dateTime.setValue(dt);
16 | expect(dateTime.getValue).to.equal(dt);
17 | });
18 |
19 | it("should only accept the DateTime", () => {
20 | expect(() => dateTime.setValue(new Date())).to.not.throw();
21 | });
22 |
23 | it("should not accept other value which are not DateTime", () => {
24 | expect(() => dateTime.setValue("string")).to.throw(InvalidFieldType);
25 | });
26 | });
27 |
28 | describe("dateTime supported options", () => {
29 | it("should not throw error if required and value set", () => {
30 | const dateTime = Field.DateTime({ required: true });
31 | dateTime.setValue(new Date());
32 | expect(() => dateTime.getValue).to.not.throw();
33 | });
34 | it("should throw error if required and no value set", () => {
35 | const dateTime = Field.DateTime({ required: true });
36 | expect(() => dateTime.getValue).to.throw(RequiredField);
37 | });
38 | it("name can be change", () => {
39 | const dateTime = Field.DateTime({ name: "custom_name" });
40 | expect(dateTime.name).to.equal("custom_name");
41 | });
42 | it("has a default value", () => {
43 | const dt = new Date();
44 | const dateTime = Field.DateTime({ default: dt });
45 | expect(dateTime.getValue).to.equal(dt);
46 | });
47 | });
48 |
49 | describe("Custom Options", () => {
50 | it("should support `auto`", () => {
51 | const textField = Field.DateTime({ auto: true });
52 | expect(textField.getValue).to.be.not.undefined;
53 | });
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/v001/fields/field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const TextField = require("../../../src/fields/TextField");
4 | const NumberField = require("../../../src/fields/NumberField");
5 | const BaseField = require("../../../src/fields/BaseField");
6 |
7 | const expect = chai.expect;
8 |
9 | describe("Field", () => {
10 | describe("Field.Text()", () => {
11 | it("should return the instance of TextField", () => {
12 | expect(Field.Text() instanceof TextField).to.be.true;
13 | });
14 | it("should also be instance of BaseField", () => {
15 | expect(Field.Text() instanceof BaseField).to.be.true;
16 | });
17 | });
18 |
19 | describe("Field.Number()", () => {
20 | it("should return the instance of NumberField", () => {
21 | expect(Field.Number() instanceof NumberField).to.be.true;
22 | });
23 | it("should also be instance of BaseField", () => {
24 | expect(Field.Number() instanceof BaseField).to.be.true;
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/v001/fields/geopoint_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const { InvalidFieldType, RequiredField } = require("../../../errors");
4 | const { Fireo } = require("../../../index");
5 |
6 | const expect = chai.expect;
7 |
8 | describe("GeoPoint", () => {
9 | describe("geoPoint() set value", () => {
10 | let geoPoint;
11 | beforeEach(() => {
12 | geoPoint = Field.GeoPoint();
13 | });
14 | it("can be set the value", () => {
15 | const gp = Fireo.GeoPoint(37.422, 122.084);
16 | geoPoint.setValue(gp);
17 | expect(geoPoint.getValue).to.equal(gp);
18 | });
19 |
20 | it("should only accept the geoPoint", () => {
21 | expect(() =>
22 | geoPoint.setValue(Fireo.GeoPoint(37.422, 122.084))
23 | ).to.not.throw();
24 | });
25 |
26 | it("should not accept other value which are not geoPoint", () => {
27 | expect(() => geoPoint.setValue("string")).to.throw(InvalidFieldType);
28 | });
29 | });
30 |
31 | describe("geoPoint supported options", () => {
32 | it("should not throw error if required and value set", () => {
33 | const geoPoint = Field.GeoPoint({ required: true });
34 | geoPoint.setValue(Fireo.GeoPoint(37.422, 122.084));
35 | expect(() => geoPoint.getValue).to.not.throw();
36 | });
37 | it("should throw error if required and no value set", () => {
38 | const geoPoint = Field.GeoPoint({ required: true });
39 | expect(() => geoPoint.getValue).to.throw(RequiredField);
40 | });
41 | it("name can be change", () => {
42 | const geoPoint = Field.GeoPoint({ name: "custom_name" });
43 | expect(geoPoint.name).to.equal("custom_name");
44 | });
45 | it("has a default value", () => {
46 | const gp = Fireo.GeoPoint(37.422, 122.084);
47 | const geoPoint = Field.GeoPoint({ default: gp });
48 | expect(geoPoint.getValue).to.deep.equal(gp);
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/test/v001/fields/id_field.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const IDField = require("../../../src/fields/IDField");
5 |
6 | const expect = Chai.expect;
7 |
8 | describe("IDField", () => {
9 | it("should return instance of IDField", () => {
10 | expect(Field.ID() instanceof IDField).to.be.true;
11 | });
12 |
13 | it("meta should contain the information about `id` field", () => {
14 | class User extends Model {
15 | id = Field.ID();
16 | }
17 |
18 | const user = User.init();
19 | expect(user.__meta.id).to.be.not.undefined;
20 | });
21 |
22 | it("should contain the name of field", () => {
23 | class User extends Model {
24 | userId = Field.ID();
25 | }
26 | const user = User.init();
27 | expect(user.__meta.id.name).to.equal("userId");
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/test/v001/fields/list_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const { InvalidFieldType, RequiredField } = require("../../../errors");
5 | const { Fireo } = require("../../../index");
6 |
7 | const expect = chai.expect;
8 |
9 | describe("ListField", () => {
10 | before(() => {
11 | Fireo.connection.setting({ projectId: "fs-test-project" });
12 | });
13 |
14 | describe("ListField() set value", () => {
15 | let listField;
16 | beforeEach(() => {
17 | listField = Field.List();
18 | });
19 | it("can be set the value", () => {
20 | listField.setValue([1, 2]);
21 | expect(listField.getValue).to.deep.equal([1, 2]);
22 | });
23 |
24 | it("should only accept the Array", () => {
25 | expect(() => listField.setValue(["value1", "value2"])).to.not.throw();
26 | });
27 |
28 | it("should not accept other value which are not Array", () => {
29 | expect(() => listField.setValue({ name: "value" })).to.throw(
30 | InvalidFieldType
31 | );
32 | });
33 | });
34 |
35 | describe("listField supported options", () => {
36 | it("should not throw error if required and value set", () => {
37 | const listField = Field.List({ required: true });
38 | listField.setValue([1, 2]);
39 | expect(() => listField.getValue).to.not.throw();
40 | });
41 | it("should throw error if required and no value set", () => {
42 | const listField = Field.List({ required: true });
43 | expect(() => listField.getValue).to.throw(RequiredField);
44 | });
45 | it("name can be change", () => {
46 | const listField = Field.List({ name: "custom_name" });
47 | expect(listField.name).to.equal("custom_name");
48 | });
49 | it("has a default value", () => {
50 | const listField = Field.List({ default: [1, "string"] });
51 | expect(listField.getValue).to.deep.equal([1, "string"]);
52 | });
53 | });
54 |
55 | describe("Update list", () => {
56 | class User extends Model {
57 | names = Field.List();
58 | }
59 |
60 | it("union", async () => {
61 | const user = User.init();
62 | user.names = ["name1", "name2"];
63 | await user.save();
64 |
65 | user.names = Fireo.listUnion("name3");
66 | await user.update();
67 |
68 | const doc = await User.collection.get({ key: user.key });
69 | expect(doc.names.length).to.equal(3);
70 | expect(doc.names.includes("name3")).to.be.true;
71 | });
72 |
73 | it("remove", async () => {
74 | const user = User.init();
75 | user.names = ["name1", "name2"];
76 | await user.save();
77 |
78 | user.names = Fireo.listRemove("name2");
79 | await user.update();
80 |
81 | const doc = await User.collection.get({ key: user.key });
82 | expect(doc.names.length).to.equal(1);
83 | expect(doc.names.includes("name2")).to.be.false;
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/test/v001/fields/map_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const { InvalidFieldType, RequiredField } = require("../../../errors");
4 |
5 | const expect = chai.expect;
6 |
7 | describe("mapField", () => {
8 | describe("mapField() set value", () => {
9 | let mapField;
10 | beforeEach(() => {
11 | mapField = Field.Map();
12 | });
13 | it("can be set the value", () => {
14 | mapField.setValue({ name: "string" });
15 | expect(mapField.getValue).to.deep.equal({ name: "string" });
16 | });
17 |
18 | it("should only accept the Object", () => {
19 | expect(() => mapField.setValue({ name: "string" })).to.not.throw();
20 | });
21 |
22 | it("should not accept other value which are not Object", () => {
23 | expect(() => mapField.setValue([1, 2])).to.throw(InvalidFieldType);
24 | });
25 | });
26 |
27 | describe("mapField supported options", () => {
28 | it("should not throw error if required and value set", () => {
29 | const mapField = Field.Map({ required: true });
30 | mapField.setValue({ name: "string" });
31 | expect(() => mapField.getValue).to.not.throw();
32 | });
33 | it("should throw error if required and no value set", () => {
34 | const mapField = Field.Map({ required: true });
35 | expect(() => mapField.getValue).to.throw(RequiredField);
36 | });
37 | it("name can be change", () => {
38 | const mapField = Field.Map({ name: "custom_name" });
39 | expect(mapField.name).to.equal("custom_name");
40 | });
41 | it("has a default value", () => {
42 | const mapField = Field.Map({ default: { name: "string" } });
43 | expect(mapField.getValue).to.deep.equal({ name: "string" });
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/test/v001/fields/number_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const { InvalidFieldType, RequiredField } = require("../../../errors");
5 | const { Fireo } = require("../../../index");
6 |
7 | const expect = chai.expect;
8 |
9 | describe("NumberField", () => {
10 | before(() => {
11 | Fireo.connection.setting({ projectId: "fs-test-project" });
12 | });
13 | it("Increment", async () => {
14 | class User extends Model {
15 | age = Field.Number();
16 | }
17 |
18 | const user = User.init();
19 | user.age = 1;
20 | await user.save();
21 |
22 | user.age = Fireo.increment(10);
23 | await user.update();
24 |
25 | const doc = await User.collection.get({ key: user.key });
26 | expect(doc.age).to.equal(11);
27 | });
28 | describe("NumberField() set value", () => {
29 | let numberField;
30 | beforeEach(() => {
31 | numberField = Field.Number();
32 | });
33 | it("can be set the value", () => {
34 | numberField.setValue(1);
35 | expect(numberField.getValue).to.equal(1);
36 | });
37 |
38 | it("should only accept the Number", () => {
39 | expect(() => numberField.setValue(1)).to.not.throw();
40 | });
41 |
42 | it("should not accept other value which are not Number", () => {
43 | expect(() => numberField.setValue("string")).to.throw(InvalidFieldType);
44 | });
45 | });
46 |
47 | describe("NumberField supported options", () => {
48 | it("should not throw error if required and value set", () => {
49 | const numberField = Field.Number({ required: true });
50 | numberField.setValue(1);
51 | expect(() => numberField.getValue).to.not.throw();
52 | });
53 | it("should throw error if required and no value set", () => {
54 | const numberField = Field.Number({ required: true });
55 | expect(() => numberField.getValue).to.throw(RequiredField);
56 | });
57 | it("name can be change", () => {
58 | const numberField = Field.Number({ name: "custom_name" });
59 | expect(numberField.name).to.equal("custom_name");
60 | });
61 | it("has a default value", () => {
62 | const numberField = Field.Number({ default: 1 });
63 | expect(numberField.getValue).to.equal(1);
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/test/v001/fields/reference_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const { RequiredField } = require("../../../errors");
5 | const firestore = require("../../../Firestore");
6 | const { DocumentReference } = require("@google-cloud/firestore");
7 | const { Fireo } = require("../../../index");
8 |
9 | const expect = chai.expect;
10 |
11 | describe("ReferenceField", () => {
12 | before(() => {
13 | Fireo.connection.setting({ projectId: "fs-test-project" });
14 | });
15 | describe("ReferenceField() set value", () => {
16 | let refField;
17 |
18 | beforeEach(() => {
19 | refField = Field.Reference();
20 | });
21 |
22 | it("can be able to set value", async () => {
23 | refField.setValue("User/custom-id");
24 | expect(refField.getValue).to.deep.equal(
25 | firestore.conn.doc("User/custom-id")
26 | );
27 | });
28 | it("should only accept key value", () => {
29 | expect(() => refField.setValue("User/custom-id")).to.not.throw();
30 | });
31 | it("should not accept value which are not key", () => {
32 | expect(() => refField.setValue("asd")).to.throw();
33 | });
34 | });
35 |
36 | describe("ReferenceField supported options", () => {
37 | it("should not throw error if required and value set", () => {
38 | const textField = Field.Reference({ required: true });
39 | textField.setValue("User/custom-id");
40 | expect(() => textField.getValue).to.not.throw();
41 | });
42 | it("should throw error if required and no value set", () => {
43 | const textField = Field.Reference({ required: true });
44 | expect(() => textField.getValue).to.throw(RequiredField);
45 | });
46 | it("name can be change", () => {
47 | const textField = Field.Reference({ name: "custom_name" });
48 | expect(textField.name).to.equal("custom_name");
49 | });
50 | it("has a default value", () => {
51 | const textField = Field.Reference({ default: "User/custom-id" });
52 | expect(textField.getValue).to.equal("User/custom-id");
53 | });
54 | });
55 |
56 | describe("Custom Options", () => {
57 | it("should return doc ref", async () => {
58 | class Company extends Model {
59 | department = Field.Text();
60 | }
61 | class Employee extends Model {
62 | name = Field.Text();
63 | company = Field.Reference();
64 | }
65 |
66 | const com = Company.init();
67 | com.department = "dept";
68 | await com.save();
69 |
70 | const emp = Employee.init();
71 | emp.name = "emp-name";
72 | emp.company = com.key;
73 | await emp.save();
74 |
75 | const doc = await Employee.collection.get({ key: emp.key });
76 | expect(doc.name).to.equal("emp-name");
77 | expect(doc.company instanceof DocumentReference);
78 |
79 | const c = await doc.company.get();
80 | expect(c.department).to.equal("dept");
81 | });
82 |
83 | it("should support `autoLoad`", async () => {
84 | class Company extends Model {
85 | department = Field.Text();
86 | }
87 | class Employee extends Model {
88 | name = Field.Text();
89 | company = Field.Reference({ autoLoad: true });
90 | }
91 |
92 | const com = Company.init();
93 | com.department = "dept";
94 | await com.save();
95 |
96 | const emp = Employee.init();
97 | emp.name = "emp-name";
98 | emp.company = com.key;
99 | await emp.save();
100 |
101 | const doc = await Employee.collection.get({ key: emp.key });
102 | // console.log(doc.company);
103 | expect(doc.name).to.equal("emp-name");
104 | expect(doc.company.department).to.equal("dept");
105 | });
106 |
107 | // it("should support `onLoad`", async () => {
108 | // class Company extends Model {
109 | // department = Field.Text();
110 | // }
111 | // class Employee extends Model {
112 | // name = Field.Text();
113 | // company = Field.Reference({ onLoad: "doSomething" });
114 |
115 | // doSomething(company) {
116 | // this.modifyDepart = company.department + "-mod";
117 | // }
118 | // }
119 |
120 | // const com = Company.init();
121 | // com.department = "dept";
122 | // await com.save();
123 |
124 | // const emp = Employee.init();
125 | // emp.name = "emp-name";
126 | // emp.company = com.key;
127 | // await emp.save();
128 |
129 | // const doc = await Employee.collection.get({ key: emp.key });
130 | // const c = await doc.company.get();
131 |
132 | // expect(doc.name).to.equal("emp-name");
133 | // expect(c.department).to.equal("dept");
134 | // expect(doc.modifyDepart).to.equal("dept-mod");
135 | // });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/test/v001/fields/text_field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const { InvalidFieldType, RequiredField } = require("../../../errors");
4 |
5 | const expect = chai.expect;
6 |
7 | describe("TextField", () => {
8 | describe("TextField() set value", () => {
9 | let textField;
10 | beforeEach(() => {
11 | textField = Field.Text();
12 | });
13 | it("can be able to set value", () => {
14 | textField.setValue("string");
15 | expect(textField.getValue).to.equal("string");
16 | });
17 | it("should only accept string value", () => {
18 | expect(() => textField.setValue("string")).to.not.throw();
19 | });
20 | it("should not accept value which are not string", () => {
21 | expect(() => textField.setValue(1)).to.throw(InvalidFieldType);
22 | });
23 | });
24 |
25 | describe("TextField supported options", () => {
26 | it("should not throw error if required and value set", () => {
27 | const textField = Field.Text({ required: true });
28 | textField.setValue("string");
29 | expect(() => textField.getValue).to.not.throw();
30 | });
31 | it("should throw error if required and no value set", () => {
32 | const textField = Field.Text({ required: true });
33 | expect(() => textField.getValue).to.throw(RequiredField);
34 | });
35 | it("name can be change", () => {
36 | const textField = Field.Text({ name: "custom_name" });
37 | expect(textField.name).to.equal("custom_name");
38 | });
39 | it("has a default value", () => {
40 | const textField = Field.Text({ default: "string" });
41 | expect(textField.getValue).to.equal("string");
42 | });
43 | });
44 |
45 | describe("Custom Options", () => {
46 | it("should support toLowercase", () => {
47 | const textField = Field.Text({ toLowercase: true });
48 | textField.setValue("STRING");
49 | expect(textField.getValue).to.equal("string");
50 | });
51 | it("should not toLowercase when option is `false`", () => {
52 | const textField = Field.Text({ toLowercase: false });
53 | textField.setValue("STRING");
54 | expect(textField.getValue).to.equal("STRING");
55 | });
56 | it("Other options does not effect on custom options", () => {
57 | const textField = Field.Text({
58 | toLowercase: true,
59 | optionNotExist: false,
60 | });
61 | textField.setValue("STRING");
62 | expect(textField.getValue).to.equal("string");
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/test/v001/firestore/batch.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const chaiAsPromised = require("chai-as-promised");
5 | const { Fireo } = require("../../../index");
6 | const { DocumentNotFound } = require("../../../errors");
7 |
8 | const expect = Chai.expect;
9 | Chai.use(chaiAsPromised);
10 |
11 | describe("Batch", () => {
12 | before(() => {
13 | Fireo.connection.setting({ projectId: "fs-test-project" });
14 | });
15 |
16 | class User extends Model {
17 | name = Field.Text();
18 | age = Field.Number();
19 | }
20 |
21 | it("Save", async () => {
22 | const batch = Fireo.batch();
23 | const user = User.init();
24 | user.name = "string";
25 | user.age = 1;
26 | user.save({ batch });
27 |
28 | await batch.commit();
29 |
30 | expect(user.key).to.be.not.undefined;
31 | });
32 | it("Upsert", async () => {
33 | const batch = Fireo.batch();
34 | const user = User.init();
35 | user.name = "string";
36 | user.age = 1;
37 | user.upsert({ batch });
38 |
39 | await batch.commit();
40 |
41 | expect(user.key).to.be.not.undefined;
42 | });
43 | it("Update", async () => {
44 | const batch = Fireo.batch();
45 | const user = User.init();
46 | user.name = "string";
47 | user.age = 1;
48 | await user.save();
49 |
50 | user.age = 2;
51 | user.update({ batch });
52 | await batch.commit();
53 |
54 | const doc = await User.collection.get({ key: user.key });
55 | expect(doc.name).to.equal("string");
56 | expect(doc.age).to.equal(2);
57 | });
58 |
59 | it("Delete", async () => {
60 | const batch = Fireo.batch();
61 | const user = User.init();
62 | user.name = "string";
63 | user.age = 1;
64 | await user.save();
65 |
66 | user.delete({ batch });
67 | await batch.commit();
68 |
69 | await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
70 | DocumentNotFound
71 | );
72 | });
73 |
74 | // it("Delete by Query", async () => {
75 | // const batch = Fireo.batch();
76 | // const user = User.init();
77 | // user.name = "string";
78 | // user.age = 1;
79 | // await user.save();
80 |
81 | // User.collection.delete({ key: user.key, batch });
82 | // await batch.commit();
83 |
84 | // await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
85 | // DocumentNotFound
86 | // );
87 | // });
88 | });
89 |
--------------------------------------------------------------------------------
/test/v001/firestore/firestore.test.js:
--------------------------------------------------------------------------------
1 | const { Firestore: CloudFirestore } = require("@google-cloud/firestore");
2 | const firestore = require("../../../Firestore");
3 | const chai = require("chai");
4 |
5 | const expect = chai.expect;
6 |
7 | describe("Firestore", () => {
8 | it("should be instance of Cloud firestore", () => {
9 | expect(firestore.conn instanceof CloudFirestore).to.be.true;
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/test/v001/firestore/firestore_operation.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const chaiAsPromised = require("chai-as-promised");
3 | const Model = require("../../../src/model/Model");
4 | const Field = require("../../../src/fields/Field");
5 | const {
6 | EmptyDocument,
7 | DocumentNotFound,
8 | KeyNotExist,
9 | } = require("../../../errors");
10 | const { Fireo } = require("../../../index");
11 |
12 | const expect = Chai.expect;
13 | Chai.use(chaiAsPromised);
14 |
15 | describe("Firestore Operation", () => {
16 | before(() => {
17 | Fireo.connection.setting({ projectId: "fs-test-project" });
18 | });
19 |
20 | //#################################
21 | //######### ID FIELD() #############
22 | //#################################
23 | describe("Model -> IDField", () => {
24 | it("should able to set custom id", async () => {
25 | class User extends Model {
26 | id = Field.ID();
27 | name = Field.Text();
28 | }
29 |
30 | const user = User.init();
31 | user.id = "custom-id";
32 | user.name = "string";
33 | await user.save();
34 |
35 | const doc = await User.collection.get({ id: "custom-id" });
36 | expect(doc.id).to.equal("custom-id");
37 | expect(doc.name).to.equal("string");
38 | });
39 |
40 | it("should able to change the id field name", async () => {
41 | class User extends Model {
42 | userId = Field.ID();
43 | name = Field.Text();
44 | }
45 |
46 | const user = User.init();
47 | user.userId = "custom-user-id";
48 | user.name = "string";
49 | await user.save();
50 |
51 | const doc = await User.collection.get({ id: "custom-user-id" });
52 | expect(doc.userId).to.equal("custom-user-id");
53 | expect(doc.name).to.equal("string");
54 | });
55 |
56 | it("should generate `id` automatically if value not provided", async () => {
57 | class User extends Model {
58 | id = Field.ID();
59 | name = Field.Text();
60 | }
61 |
62 | const user = User.init();
63 | user.name = "string";
64 | await user.save();
65 |
66 | const doc = await User.collection.get({ id: user.id });
67 | expect(doc.id).to.equal(user.id);
68 | expect(doc.name).to.equal("string");
69 | });
70 | });
71 |
72 | //#################################
73 | //######### SAVE() ################
74 | //#################################
75 |
76 | describe("Save()", () => {
77 | let user;
78 | beforeEach(() => {
79 | class User extends Model {
80 | name = Field.Text();
81 | }
82 |
83 | user = User.init();
84 | });
85 |
86 | it("should save data", async () => {
87 | user.name = "string";
88 | await user.save();
89 | });
90 | it("should not save the empty document", async () => {
91 | await expect(user.save()).to.be.rejectedWith(EmptyDocument);
92 | });
93 | it("should return `id` in model", async () => {
94 | user.name = "string";
95 | await user.save();
96 |
97 | expect(user.id).to.be.not.undefined;
98 | });
99 | it("should return `key` in model", async () => {
100 | user.name = "string";
101 | await user.save();
102 |
103 | expect(user.key).to.be.not.undefined;
104 | });
105 |
106 | it("should able to save zero in Number Field", async () => {
107 | class User extends Model {
108 | name = Field.Text();
109 | age = Field.Number();
110 | }
111 | const user = User.init();
112 | user.name = "string";
113 | user.age = 0;
114 | await user.save();
115 |
116 | const doc = await User.collection.get({ key: user.key });
117 | expect(doc.name).to.equal("string");
118 | expect(doc.age).to.equal(0);
119 | });
120 | });
121 |
122 | //#################################
123 | //######### UPSERT() ##############
124 | //#################################
125 |
126 | describe("Upsert()", () => {
127 | it("should add document if not exist", async () => {
128 | class User extends Model {
129 | name = Field.Text();
130 | age = Field.Number();
131 | }
132 |
133 | const user = User.init();
134 | user.name = "string";
135 | user.age = 1;
136 | await user.upsert();
137 |
138 | expect(user.id).to.be.not.undefined;
139 | expect(user.key).to.be.not.undefined;
140 | });
141 | it("should merge document if exist", async () => {
142 | class User extends Model {
143 | id = Field.ID();
144 | name = Field.Text();
145 | age = Field.Number();
146 | }
147 |
148 | const user = User.init();
149 | user.id = "upsert-custom-id";
150 | user.name = "string";
151 | await user.save();
152 |
153 | const userUpdate = User.init();
154 | userUpdate.id = "upsert-custom-id";
155 | userUpdate.age = 1;
156 | await userUpdate.upsert();
157 |
158 | const doc = await User.collection.get({ id: "upsert-custom-id" });
159 | expect(doc.name).to.equal("string");
160 | expect(doc.age).to.equal(1);
161 | });
162 | });
163 |
164 | //#################################
165 | //######### GET() #################
166 | //#################################
167 |
168 | describe("Get()", () => {
169 | it("should return instance of model", async () => {
170 | class User extends Model {
171 | name = Field.Text();
172 | }
173 |
174 | const user = User.init();
175 | user.name = "string";
176 | await user.save();
177 |
178 | const doc = await User.collection.get({ id: user.id });
179 | expect(doc instanceof User).to.be.true;
180 | expect(doc.id).to.be.not.undefined;
181 | expect(doc.key).to.be.not.undefined;
182 | });
183 |
184 | it("should able to get data with id", async () => {
185 | class User extends Model {
186 | name = Field.Text();
187 | }
188 |
189 | const user = User.init();
190 | user.name = "string";
191 | await user.save();
192 |
193 | const doc = await User.collection.get({ id: user.id });
194 | expect(doc.id).to.equal(user.id);
195 | expect(doc.name).to.equal("string");
196 | });
197 | it("should able to get data with custom id", async () => {
198 | class User extends Model {
199 | userId = Field.ID();
200 | name = Field.Text();
201 | }
202 |
203 | const user = User.init();
204 | user.userId = "some-custom-id";
205 | user.name = "string";
206 | await user.save();
207 |
208 | const doc = await User.collection.get({ id: user.userId });
209 | expect(doc.userId).to.equal(user.userId);
210 | expect(doc.name).to.equal("string");
211 | });
212 | it("should able to get data with key", async () => {
213 | class User extends Model {
214 | name = Field.Text();
215 | }
216 |
217 | const user = User.init();
218 | user.name = "string";
219 | await user.save();
220 |
221 | const doc = await User.collection.get({ key: user.key });
222 | expect(doc.userId).to.equal(user.userId);
223 | expect(doc.key).to.equal(user.key);
224 | expect(doc.name).to.equal("string");
225 | });
226 | });
227 |
228 | //#################################
229 | //######### UPSERT() ##############
230 | //#################################
231 |
232 | describe("Update()", () => {
233 | class User extends Model {
234 | name = Field.Text();
235 | age = Field.Number();
236 | }
237 |
238 | it("should update the existing document", async () => {
239 | const user = User.init();
240 | user.name = "string";
241 | user.age = 1;
242 | await user.save();
243 |
244 | user.name = "updated-name";
245 | await user.update();
246 |
247 | const doc = await User.collection.get({ id: user.id });
248 | expect(doc.name).to.equal("updated-name");
249 | expect(doc.age).to.equal(1);
250 | });
251 | it("throw error if document key not exists and not provided in `update()`", async () => {
252 | const user = User.init();
253 | user.name = "string";
254 | user.age = 1;
255 | await expect(user.update()).to.be.rejectedWith(KeyNotExist);
256 | });
257 | it("throw error if document not exists", async () => {
258 | const user = User.init();
259 | user.name = "string";
260 | user.age = 1;
261 | await expect(
262 | user.update({ id: "id-should-not-exist" })
263 | ).to.be.rejectedWith(DocumentNotFound);
264 | });
265 | it("update document through `id`", async () => {
266 | const user = User.init();
267 | user.name = "string";
268 | user.age = 1;
269 | await user.save();
270 |
271 | const updateUser = User.init();
272 | updateUser.name = "updated-name";
273 | await updateUser.update({ id: user.id });
274 | });
275 | it("update document through `key`", async () => {
276 | const user = User.init();
277 | user.name = "string";
278 | user.age = 1;
279 | await user.save();
280 |
281 | const updateUser = User.init();
282 | updateUser.name = "updated-name";
283 | await updateUser.update({ key: user.key });
284 | });
285 | });
286 |
287 | //#################################
288 | //######### DELETE() ##############
289 | //#################################
290 |
291 | describe("Delete()", () => {
292 | class User extends Model {
293 | name = Field.Text();
294 | }
295 |
296 | it("should be called from model object", async () => {
297 | const user = User.init();
298 | user.name = "string";
299 | await user.save();
300 |
301 | await user.delete();
302 |
303 | await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
304 | DocumentNotFound
305 | );
306 | });
307 |
308 | it("throw error if key not defined", async () => {
309 | const user = User.init();
310 | user.name = "string";
311 |
312 | await expect(user.delete()).to.be.rejectedWith(KeyNotExist);
313 | });
314 |
315 | it("should able to call it from `collection`", async () => {
316 | const user = User.init();
317 | user.name = "string";
318 | await user.save();
319 |
320 | await User.collection.delete({ key: user.key });
321 |
322 | await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
323 | DocumentNotFound
324 | );
325 | });
326 |
327 | it("delete using `id` from `collection`", async () => {
328 | const user = User.init();
329 | user.name = "string";
330 | await user.save();
331 |
332 | await User.collection.delete({ id: user.id });
333 |
334 | await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
335 | DocumentNotFound
336 | );
337 | });
338 |
339 | it("should delete all child documents", async () => {
340 | class User extends Model {
341 | name = Field.Text();
342 | }
343 | class Address extends Model {
344 | location = Field.Text();
345 | }
346 |
347 | const user = User.init();
348 | user.name = "string";
349 | await user.save();
350 |
351 | const address = Address.init({ parent: user.key });
352 | address.location = "user-location";
353 | await address.save();
354 |
355 | await User.collection.delete({ key: user.key, child: true });
356 |
357 | await expect(
358 | Address.collection.get({ key: address.key })
359 | ).to.be.rejectedWith(DocumentNotFound);
360 | await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
361 | DocumentNotFound
362 | );
363 | });
364 |
365 | it("delete entire collection", async () => {
366 | class City extends Model {
367 | name = Field.Text();
368 | }
369 | const city = City.init();
370 | city.name = "city1";
371 | await city.save();
372 |
373 | const city2 = City.init();
374 | city2.name = "city2";
375 | await city2.save();
376 |
377 | await City.collection.delete();
378 |
379 | const doc = await City.collection.fetch(1);
380 | expect(doc.list.length).to.equal(0);
381 | });
382 |
383 | it("delete entire collection and its child", async () => {
384 | class Parent extends Model {
385 | name = Field.Text();
386 | }
387 |
388 | class Child extends Model {
389 | name = Field.Text();
390 | }
391 |
392 | const p = Parent.init();
393 | p.name = "string";
394 | await p.save();
395 |
396 | const c = Child.init({ parent: p.key });
397 | c.name = "string";
398 | await c.save();
399 |
400 | await Parent.collection.delete({ child: true });
401 |
402 | await expect(Parent.collection.get({ key: p.key })).to.rejectedWith(
403 | DocumentNotFound
404 | );
405 | await expect(Child.collection.get({ key: c.key })).to.rejectedWith(
406 | DocumentNotFound
407 | );
408 | });
409 |
410 | it("should delete the docs by query", async () => {
411 | const user = User.init();
412 | user.name = "by-query";
413 | await user.save();
414 |
415 | await User.collection.where("name", "==", "by-query").delete();
416 |
417 | await expect(User.collection.get({ key: user.key })).to.rejectedWith(
418 | DocumentNotFound
419 | );
420 | });
421 | });
422 | });
423 |
--------------------------------------------------------------------------------
/test/v001/firestore/query.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Fields = require("../../../src/fields/Field");
4 | const Field = require("../../../src/fields/Field");
5 | const { Fireo } = require("../../../index");
6 |
7 | const expect = Chai.expect;
8 |
9 | describe("Query", () => {
10 | class User extends Model {
11 | name = Field.Text();
12 | age = Fields.Number();
13 | address = Field.Text({ name: "location" });
14 | }
15 |
16 | before(() => {
17 | Fireo.connection.setting({ projectId: "fs-test-project" });
18 |
19 | return new Promise((resolve) => {
20 | setTimeout(() => {
21 | const user = User.init();
22 | user.name = "name1";
23 | user.age = 1;
24 | user.address = "address1";
25 | user.save();
26 |
27 | const user2 = User.init();
28 | user2.name = "name2";
29 | user2.age = 1;
30 | user2.address = "address2";
31 | user2.save();
32 |
33 | const user3 = User.init();
34 | user3.name = "name2";
35 | user3.age = 2;
36 | user3.address = "address1";
37 | user3.save();
38 | resolve();
39 | }, 500);
40 | });
41 | });
42 |
43 | it("should able to filter", async () => {
44 | const docs = await User.collection.where("name", "==", "name1").fetch();
45 | expect(docs.list.length).to.greaterThan(0);
46 | for (const doc of docs.list) {
47 | expect(doc.name).to.equal("name1");
48 | }
49 | });
50 |
51 | it("should able to apply multiple filters", async () => {
52 | const docs = await User.collection
53 | .where("name", "==", "name2")
54 | .where("age", "==", 1)
55 | .fetch();
56 | expect(docs.list.length).to.greaterThan(0);
57 | for (const doc of docs.list) {
58 | expect(doc.name).to.equal("name2");
59 | expect(doc.age).to.equal(1);
60 | }
61 | });
62 |
63 | it("should able to filter with custom field name", async () => {
64 | const docs = await User.collection
65 | .where("address", "==", "address1")
66 | .fetch();
67 | expect(docs.list.length).to.greaterThan(0);
68 | for (const doc of docs.list) {
69 | expect(doc.address).to.equal("address1");
70 | }
71 | });
72 | it("should able to get first document from query", async () => {
73 | const doc = await User.collection.where("age", "==", 1).get();
74 | expect(doc.key).to.be.not.undefined;
75 | expect(doc.age).to.equal(1);
76 | });
77 |
78 | it("should able to fetch all documents", async () => {
79 | const docs = await User.collection.fetch();
80 | expect(docs.list.length).to.greaterThan(2);
81 | });
82 |
83 | it("should able to fetch limited documents", async () => {
84 | const docs = await User.collection.fetch(2);
85 | expect(docs.list.length).to.equal(2);
86 | });
87 |
88 | it("should able to limit data", async () => {
89 | const docs = await User.collection.limit(1).fetch();
90 | expect(docs.list.length).to.equal(1);
91 | expect(docs.list[0].key).to.be.not.undefined;
92 | });
93 | it("should able to limit data in fetch", async () => {
94 | const docs = await User.collection.fetch(1);
95 | expect(docs.list.length).to.equal(1);
96 | expect(docs.list[0].key).to.be.not.undefined;
97 | });
98 | it("should able to order the collection", async () => {
99 | const docs = await User.collection.orderBy("age").fetch(1);
100 | expect(docs.list.length).to.greaterThan(0);
101 | let previousAge = 0;
102 | for (const doc of docs.list) {
103 | expect(doc.age).to.gte(previousAge);
104 | previousAge = doc.age;
105 | }
106 | });
107 | it("should return the empty array", async () => {
108 | const docs = await User.collection.where("name", "==", "not-exist").fetch();
109 | expect(docs.list.length).to.equal(0);
110 | });
111 | it("should order the results", async () => {
112 | const docs = await User.collection
113 | .where("age", "==", 1)
114 | .orderBy("age")
115 | .fetch();
116 | expect(docs.list.length).to.greaterThan(0);
117 | let previousAge = 0;
118 | for (const doc of docs.list) {
119 | expect(doc.age).to.gte(previousAge);
120 | previousAge = doc.age;
121 | }
122 | });
123 | it("should order the results in descending order", async () => {
124 | const docs = await User.collection
125 | .where("age", "==", 1)
126 | .orderBy("-age")
127 | .fetch();
128 | expect(docs.list.length).to.greaterThan(0);
129 | let previousAge = 3;
130 | for (const doc of docs.list) {
131 | expect(doc.age).to.lte(previousAge);
132 | previousAge = doc.age;
133 | }
134 | });
135 |
136 | it("should able to offset data", async () => {
137 | class UserOffset extends Model {
138 | age = Field.Number();
139 | }
140 | for (let i = 1; i < 10; i++) {
141 | const u = UserOffset.init();
142 | u.age = new Date().getTime();
143 | await u.save();
144 | }
145 |
146 | const firstList = await UserOffset.collection.orderBy("age").fetch(3);
147 | const lastQueryAge = firstList.list[2].age;
148 |
149 | const secondList = await UserOffset.collection
150 | .orderBy("age")
151 | .offset(3)
152 | .fetch(3);
153 | expect(secondList.list[1].age).to.greaterThan(lastQueryAge);
154 | });
155 |
156 | describe("Cursor", () => {
157 | class UserCursor extends Model {
158 | name = Field.Text();
159 | age = Field.Number();
160 | address = Field.Text({ name: "location" });
161 | }
162 |
163 | before(() => {
164 | return new Promise((resolve) => {
165 | setTimeout(() => {
166 | for (let i = 1; i < 10; i++) {
167 | const u = UserCursor.init();
168 | u.name = "name" + i;
169 | u.address = "address";
170 | u.age = new Date().getTime();
171 | u.save();
172 | }
173 |
174 | resolve();
175 | }, 200);
176 | });
177 | });
178 |
179 | it("should able to create query cursor", async () => {
180 | const query = await UserCursor.collection
181 | .where("address", "==", "address")
182 | .orderBy("age")
183 | .limit(3)
184 | .fetch();
185 | expect(query.cursor).to.be.not.undefined;
186 |
187 | const lastDocAge = query.list[query.list.length - 1].age;
188 |
189 | const nextQuery = await UserCursor.collection
190 | .cursor(query.cursor)
191 | .fetch();
192 | expect(nextQuery.list.length).to.equal(3);
193 | expect(nextQuery.list[0].age > lastDocAge);
194 | });
195 |
196 | it("should able to modify the limit of query cursor", async () => {
197 | const query = await UserCursor.collection
198 | .where("address", "==", "address")
199 | .orderBy("age")
200 | .limit(3)
201 | .fetch();
202 |
203 | const nextQuery = await UserCursor.collection
204 | .cursor(query.cursor)
205 | .fetch(4);
206 | expect(nextQuery.list.length).to.equal(4);
207 | });
208 |
209 | it("should able to query in child collections", async () => {
210 | class User extends Model {
211 | name = Field.Text();
212 | }
213 | class Address extends Model {
214 | location = Field.Text();
215 | }
216 |
217 | const user = User.init();
218 | user.name = "string";
219 | await user.save();
220 |
221 | const address = Address.init({ parent: user.key });
222 | address.location = "user-location";
223 | await address.save();
224 |
225 | const doc = await Address.collection
226 | .parent(user.key)
227 | .where("location", "==", "user-location")
228 | .fetch(1);
229 | expect(doc.list[0].location).to.equal("user-location");
230 | });
231 | });
232 | });
233 |
--------------------------------------------------------------------------------
/test/v001/firestore/sub_collection.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Fields = require("../../../src/fields/Field");
4 | const { Fireo } = require("../../../index");
5 |
6 | const expect = Chai.expect;
7 |
8 | describe("Sub Collection", () => {
9 | before(() => {
10 | Fireo.connection.setting({ projectId: "fs-test-project" });
11 | });
12 |
13 | class User extends Model {
14 | name = Fields.Text();
15 | }
16 | class Address extends Model {
17 | city = Fields.Text();
18 | }
19 | it("should able to save and get data", async () => {
20 | const user = User.init();
21 | user.name = "string";
22 | await user.save();
23 |
24 | const address = Address.init({ parent: user.key });
25 | address.city = "city";
26 | await address.save();
27 |
28 | const doc = await Address.collection.get({ key: address.key });
29 | expect(doc.city).to.equal("city");
30 | });
31 | it("should able to save and get data with custom `id`", async () => {
32 | class User extends Model {
33 | id = Fields.ID();
34 | name = Fields.Text();
35 | }
36 | class Address extends Model {
37 | id = Fields.ID();
38 | city = Fields.Text();
39 | }
40 |
41 | const user = User.init();
42 | user.id = "custom-id-field";
43 | user.name = "string";
44 | await user.save();
45 |
46 | const address = Address.init({ parent: user.key });
47 | address.id = "custom-address-id";
48 | address.city = "city";
49 | await address.save();
50 |
51 | const doc = await Address.collection.get({ key: address.key });
52 | expect(doc.city).to.equal("city");
53 | });
54 | it("should able to delete sub document", async () => {
55 | const user = User.init();
56 | user.name = "string";
57 | await user.save();
58 |
59 | const address = Address.init({ parent: user.key });
60 | address.city = "city";
61 | await address.save();
62 |
63 | await address.delete();
64 |
65 | const doc = await User.collection.get({ key: user.key });
66 | expect(doc.name).to.equal("string");
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/test/v001/firestore/transactions.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const chaiAsPromised = require("chai-as-promised");
5 | const { Fireo } = require("../../../index");
6 | const { DocumentNotFound } = require("../../../errors");
7 |
8 | const expect = Chai.expect;
9 | Chai.use(chaiAsPromised);
10 |
11 | describe("Transactions", async () => {
12 | before(() => {
13 | Fireo.connection.setting({ projectId: "fs-test-project" });
14 | });
15 |
16 | class User extends Model {
17 | name = Field.Text();
18 | }
19 | it("Get", async () => {
20 | const user = User.init();
21 | user.name = "string";
22 | await user.save();
23 |
24 | const userKey = user.key;
25 | const name = await Fireo.runTransaction(async (t) => {
26 | const doc = await User.collection.get({ key: userKey, transaction: t });
27 | return doc.name;
28 | });
29 | expect(name).to.equal("string");
30 | });
31 | it("Save", async () => {
32 | const userKey = await Fireo.runTransaction(async (t) => {
33 | const user = User.init();
34 | user.name = "string";
35 | await user.save({ transaction: t });
36 | return user.key;
37 | });
38 | const doc = await User.collection.get({ key: userKey });
39 | expect(doc.name).to.equal("string");
40 | });
41 |
42 | it("Upsert", async () => {
43 | const userKey = await Fireo.runTransaction(async (t) => {
44 | const user = User.init();
45 | user.name = "string";
46 | await user.upsert({ transaction: t });
47 | return user.key;
48 | });
49 | const doc = await User.collection.get({ key: userKey });
50 | expect(doc.name).to.equal("string");
51 | });
52 | it("Delete", async () => {
53 | const userKey = await Fireo.runTransaction(async (t) => {
54 | const user = User.init();
55 | user.name = "string";
56 | await user.save({ transaction: t });
57 |
58 | await user.delete({ transaction: t });
59 | return user.key;
60 | });
61 |
62 | await expect(User.collection.get({ key: userKey })).to.be.rejectedWith(
63 | DocumentNotFound
64 | );
65 | });
66 |
67 | it("Update", async () => {
68 | const user = User.init();
69 | user.name = "string";
70 | await user.save();
71 |
72 | const userKey = await Fireo.runTransaction(async (t) => {
73 | user.name = "updated-string";
74 | await user.update({ transaction: t });
75 |
76 | return user.key;
77 | });
78 |
79 | const doc = await User.collection.get({ key: userKey });
80 | expect(doc.name).to.equal("updated-string");
81 | });
82 |
83 | it("Combine operations", async () => {
84 | const user = User.init();
85 | user.name = "string";
86 | await user.save();
87 | const userKey = user.key;
88 |
89 | const docKey = await Fireo.runTransaction(async (t) => {
90 | const doc = await User.collection.get({ key: userKey, transaction: t });
91 | expect(doc.key).to.equal(userKey);
92 |
93 | const user = User.init();
94 | user.name = "string";
95 | await user.save({ transaction: t });
96 | await user.delete({ transaction: t });
97 |
98 | return user.key;
99 | });
100 |
101 | await expect(User.collection.get({ key: docKey })).to.be.rejectedWith(
102 | DocumentNotFound
103 | );
104 | });
105 |
106 | describe("Query", () => {
107 | class User extends Model {
108 | name = Field.Text();
109 | }
110 |
111 | it("fetch", async () => {
112 | const user = User.init();
113 | user.name = "trans-name";
114 | await user.save();
115 |
116 | const docName = await Fireo.runTransaction(async (t) => {
117 | const doc = await User.collection
118 | .transaction(t)
119 | .where("name", "==", "trans-name")
120 | .get();
121 |
122 | return doc.name;
123 | });
124 |
125 | expect(docName).to.equal("trans-name");
126 | });
127 | // it("Delete", async () => {
128 | // const user = User.init();
129 | // user.name = "trans-name-delete";
130 | // await user.save();
131 |
132 | // await Fireo.runTransaction(async (t) => {
133 | // await User.collection.transaction(t).
134 | // .where("name", "==", "trans-name-delete")
135 | // .delete();
136 | // });
137 |
138 | // await expect(User.collection.get({ key: user.key })).to.be.rejectedWith(
139 | // DocumentNotFound
140 | // );
141 | // });
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/test/v001/model/model.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const { InstantiateError } = require("../../../errors");
5 | const firestore = require("../../../Firestore");
6 | const { Fireo } = require("../../../index");
7 |
8 | const expect = chai.expect;
9 |
10 | describe("Model", () => {
11 | before(() => {
12 | Fireo.connection.setting({ projectId: "fs-test-project" });
13 | });
14 |
15 | class User extends Model {
16 | name = Field.Text();
17 | }
18 |
19 | describe("Model creation", () => {
20 | class User extends Model {
21 | name = Field.Text();
22 | age = Field.Number();
23 | }
24 |
25 | it("create model from `fromObject` should return Model instance", () => {
26 | const user = User.fromObject({ name: "string", age: 1 });
27 | expect(user instanceof User).to.be.true;
28 | });
29 | it("create model from `fromObject`", () => {
30 | const user = User.fromObject({ name: "string", age: 1 });
31 | expect(user.name).to.equal("string");
32 | expect(user.age).to.equal(1);
33 | });
34 | it("create model for parent from `fromObject`", () => {
35 | const user = User.parent("User/custom-id").fromObject({
36 | name: "string",
37 | age: 1,
38 | });
39 |
40 | expect(user.name).to.equal("string");
41 | expect(user.age).to.equal(1);
42 | expect(user.__meta.parent).to.equal("User/custom-id");
43 | });
44 | it("Convert model into object `toObject`", async () => {
45 | const user = User.init();
46 | user.name = "string";
47 | user.age = 1;
48 | await user.save();
49 |
50 | const doc = await User.collection.get({ key: user.key });
51 | const docObject = doc.toObject();
52 | expect(docObject).to.deep.equal({
53 | name: "string",
54 | age: 1,
55 | id: user.id,
56 | key: user.key,
57 | });
58 | });
59 | });
60 |
61 | it("should be instantiate from `init()`", () => {
62 | const user = User.init();
63 | expect(user.__meta.isInstantiate).be.true;
64 | });
65 | it("should not be instantiate from `new` keyword", () => {
66 | expect(() => new User()).to.throw(InstantiateError);
67 | });
68 | it("`init()` method should return the instance of class", () => {
69 | expect(User.init() instanceof User).to.be.true;
70 | });
71 | it("after getting fields record every field should be undefined", () => {
72 | const user = User.init();
73 | expect(user.name).to.be.undefined;
74 | });
75 |
76 | it("able to set custom collection", async () => {
77 | User.config = {
78 | collectionName: "custom_collection",
79 | };
80 |
81 | const user = User.init();
82 | user.name = "string";
83 | await user.save();
84 |
85 | const snapshot = await firestore.conn
86 | .collection("custom_collection")
87 | .doc(user.id)
88 | .get();
89 | const doc = snapshot.data();
90 | expect(doc.name).to.equal("string");
91 | });
92 |
93 | it("able to set lowercase values", async () => {
94 | User.config = {
95 | toLowercase: true,
96 | };
97 |
98 | const user = User.init();
99 | user.name = "STRING-VALUE";
100 | await user.save();
101 |
102 | const doc = await User.collection.where("name", "==", "String-Value").get();
103 | expect(doc.name).to.equal("string-value");
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/test/v001/model/modelmeta.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const Model = require("../../../src/model/Model");
3 | const Field = require("../../../src/fields/Field");
4 | const TextField = require("../../../src/fields/TextField");
5 |
6 | const expect = chai.expect;
7 |
8 | describe("Meta Model", () => {
9 | it("should keep the record of collectionName", () => {
10 | class User extends Model {
11 | name = Field.Text();
12 | }
13 |
14 | const user = User.init();
15 | expect(user.__meta.collectionName).to.be.equal("User");
16 | });
17 | it("should keep the record of Model Name", () => {
18 | class User extends Model {
19 | name = Field.Text();
20 | }
21 |
22 | const user = User.init();
23 | expect(user.__meta.modelName).to.be.equal("User");
24 | });
25 |
26 | describe("Config", () => {
27 | class User extends Model {
28 | name = Field.Text();
29 | }
30 |
31 | it("should able to set custom collection name", () => {
32 | User.config = {
33 | collectionName: "custom_collection",
34 | };
35 |
36 | const user = User.init();
37 | expect(user.__meta.collectionName).to.equal("custom_collection");
38 | });
39 |
40 | it("should able to contain the config object", () => {
41 | User.config = {
42 | collectionName: "custom_collection",
43 | toLowercase: true,
44 | };
45 |
46 | const user = User.init();
47 | expect(user.__meta.config).to.equal(User.config);
48 | });
49 | });
50 |
51 | describe("Model -> fields", () => {
52 | class User extends Model {
53 | name = Field.Text();
54 | age = Field.Number();
55 | }
56 | it("should keep the record of fields", () => {
57 | const user = User.init();
58 | expect(user.__meta.fields.name instanceof TextField).to.be.true;
59 | });
60 | it("should have model name", () => {
61 | const user = User.init();
62 | expect(user.__meta.fields.name.modelName).to.equal("User");
63 | });
64 | it("should be able to set value from model", () => {
65 | const user = User.init();
66 | user.name = "string";
67 | user.age = 1;
68 | user.__parseField();
69 |
70 | expect(user.__meta.fields.name.val).to.equal("string");
71 | expect(user.__meta.fields.age.val).to.equal(1);
72 | });
73 |
74 | it("should contains the custom name and original name", () => {
75 | class User extends Model {
76 | name = Field.Text({ name: "custom_name" });
77 | }
78 |
79 | const user = User.init();
80 | user.name = "string";
81 | user.__parseField();
82 |
83 | expect(user.__meta.fields.name.originalName).to.equal("name");
84 | expect(user.__meta.fields.name.customName).to.equal("custom_name");
85 | });
86 |
87 | it("should have same name when there is no custom name set", () => {
88 | class User extends Model {
89 | name = Field.Text();
90 | }
91 |
92 | const user = User.init();
93 | user.name = "string";
94 | user.__parseField();
95 |
96 | expect(user.__meta.fields.name.name).to.equal("name");
97 | });
98 |
99 | it("should have same custom name when there is no custom name set", () => {
100 | class User extends Model {
101 | name = Field.Text({ name: "custom_name" });
102 | }
103 |
104 | const user = User.init();
105 | user.name = "string";
106 | user.__parseField();
107 |
108 | expect(user.__meta.fields.name.name).to.equal("custom_name");
109 | });
110 |
111 | it("parse fields", () => {
112 | class User extends Model {
113 | name = Field.Text();
114 | age = Field.Number();
115 | address = Field.Text({ name: "location" });
116 | }
117 |
118 | const user = User.init();
119 | user.name = "string";
120 | user.age = 1;
121 | user.address = "user-location";
122 | user.__parseField();
123 |
124 | expect(user.__meta.parseFields.name).to.equal("string");
125 | expect(user.__meta.parseFields.age).to.equal(1);
126 | expect(user.__meta.parseFields.location).to.equal("user-location");
127 | });
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/test/v001/v1.0.up/limit-to-last.test.js:
--------------------------------------------------------------------------------
1 | const Chai = require("chai");
2 | const Field = require("../../../src/fields/Field");
3 | const Model = require("../../../src/model/Model");
4 |
5 | const expect = Chai.expect;
6 |
7 | describe("Limit To Last", () => {
8 | class LimitModel extends Model {
9 | number = Field.Number();
10 | collection_number = Field.Number();
11 | content = Field.Text();
12 | }
13 | it("should able to limit docs in reverse order", async () => {
14 | for (let i = 0; i <= 5; i++) {
15 | const doc = LimitModel.init();
16 | doc.number = i;
17 | doc.collection_number = 1;
18 | doc.content = "content " + i;
19 | await doc.save();
20 | }
21 |
22 | const docs = await LimitModel.collection
23 | .where("collection_number", "==", 1)
24 | .orderBy("number")
25 | .limitToLast(3)
26 | .fetch();
27 |
28 | expect(docs.list.length).to.equal(3);
29 | expect(docs.list[0].number).to.equal(3);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------