├── docs ├── index │ ├── why.md │ ├── what.md │ └── install.md ├── guides.md ├── guides │ └── index.md ├── docs.md ├── api │ ├── auth │ │ ├── methods │ │ │ ├── popup.md │ │ │ ├── anonymous.md │ │ │ ├── token.md │ │ │ ├── popup │ │ │ │ └── google.md │ │ │ └── email.md │ │ ├── methods.md │ │ └── user.md │ ├── firestore.md │ ├── firestore │ │ ├── reference.md │ │ ├── reference │ │ │ ├── condition.md │ │ │ ├── collection-group.md │ │ │ └── collection.md │ │ ├── batch.md │ │ └── transaction.md │ ├── functions │ │ └── region.md │ ├── utils.md │ ├── functions.md │ ├── storage.md │ ├── auth.md │ ├── object.md │ ├── stores.md │ ├── stores │ │ └── stats.md │ ├── initialize.md │ └── models.md ├── api.md ├── decorators.md └── decorators │ ├── route.md │ ├── root.md │ ├── model.md │ ├── activate.md │ └── models.md ├── firebase ├── functions │ ├── .gitignore │ ├── index.js │ └── package.json ├── standalone │ ├── .gitgnore │ ├── package.json │ └── lib │ │ ├── create-token.js │ │ ├── update-user-email.js │ │ ├── create-sign-in-link.js │ │ ├── delete-users.js │ │ └── setup.js ├── README.md ├── cors.json ├── .firebaserc ├── package.json ├── storage.rules ├── firebase.json ├── firestore.indexes.json ├── firestore.rules └── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── tests ├── dummy │ ├── app │ │ ├── templates │ │ │ ├── docs │ │ │ │ ├── index-loading.hbs │ │ │ │ ├── page-loading.hbs │ │ │ │ ├── index.hbs │ │ │ │ └── page │ │ │ │ │ └── index.hbs │ │ │ ├── playground │ │ │ │ ├── auth.hbs │ │ │ │ ├── dev.hbs │ │ │ │ ├── index.hbs │ │ │ │ ├── content.hbs │ │ │ │ ├── models.hbs │ │ │ │ ├── storage.hbs │ │ │ │ ├── document.hbs │ │ │ │ ├── functions.hbs │ │ │ │ ├── reordering.hbs │ │ │ │ ├── query │ │ │ │ │ ├── array.hbs │ │ │ │ │ └── single.hbs │ │ │ │ ├── route.hbs │ │ │ │ ├── messages │ │ │ │ │ ├── index.hbs │ │ │ │ │ └── message │ │ │ │ │ │ └── index.hbs │ │ │ │ └── messages.hbs │ │ │ ├── missing.hbs │ │ │ ├── application.hbs │ │ │ ├── playground.hbs │ │ │ ├── docs.hbs │ │ │ └── index.hbs │ │ ├── components │ │ │ ├── block │ │ │ │ ├── master-detail │ │ │ │ │ └── section.hbs │ │ │ │ ├── stalled.hbs │ │ │ │ ├── changes │ │ │ │ │ ├── property.hbs │ │ │ │ │ └── property.js │ │ │ │ ├── remark │ │ │ │ │ ├── index.hbs │ │ │ │ │ ├── link-to.hbs │ │ │ │ │ └── link-to.js │ │ │ │ ├── toc │ │ │ │ │ ├── page.hbs │ │ │ │ │ └── pages.hbs │ │ │ │ ├── index │ │ │ │ │ └── section.hbs │ │ │ │ ├── master-detail.hbs │ │ │ │ ├── toc.js │ │ │ │ ├── changes.hbs │ │ │ │ ├── message.js │ │ │ │ ├── toc.hbs │ │ │ │ ├── message.hbs │ │ │ │ ├── playground │ │ │ │ │ └── navigation.hbs │ │ │ │ └── changes.js │ │ │ ├── json.hbs │ │ │ ├── route │ │ │ │ ├── playground │ │ │ │ │ ├── index.hbs │ │ │ │ │ ├── messages │ │ │ │ │ │ ├── index.hbs │ │ │ │ │ │ └── message │ │ │ │ │ │ │ └── index.hbs │ │ │ │ │ ├── query │ │ │ │ │ │ ├── single.hbs │ │ │ │ │ │ ├── array.hbs │ │ │ │ │ │ ├── array.js │ │ │ │ │ │ └── single.js │ │ │ │ │ ├── route.js │ │ │ │ │ ├── dev.hbs │ │ │ │ │ ├── route.hbs │ │ │ │ │ ├── document.hbs │ │ │ │ │ ├── functions.hbs │ │ │ │ │ ├── content.hbs │ │ │ │ │ ├── models.hbs │ │ │ │ │ ├── dev.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── messages.hbs │ │ │ │ │ ├── document.js │ │ │ │ │ ├── functions.js │ │ │ │ │ ├── content.js │ │ │ │ │ ├── reordering.hbs │ │ │ │ │ ├── models.js │ │ │ │ │ ├── storage.hbs │ │ │ │ │ ├── storage.js │ │ │ │ │ └── auth.hbs │ │ │ │ ├── docs │ │ │ │ │ ├── index.hbs │ │ │ │ │ └── page │ │ │ │ │ │ └── index.hbs │ │ │ │ ├── docs.hbs │ │ │ │ ├── missing.hbs │ │ │ │ ├── index.js │ │ │ │ ├── playground.hbs │ │ │ │ └── index.hbs │ │ │ ├── json.js │ │ │ ├── docs │ │ │ │ └── route.hbs │ │ │ └── input │ │ │ │ ├── file.hbs │ │ │ │ └── file.js │ │ ├── models │ │ │ ├── fancy-message.js │ │ │ ├── user.js │ │ │ ├── pages │ │ │ │ └── messages │ │ │ │ │ ├── message │ │ │ │ │ └── index.js │ │ │ │ │ └── message.js │ │ │ ├── post.js │ │ │ ├── message.js │ │ │ ├── messages.js │ │ │ └── docs │ │ │ │ └── page.js │ │ ├── styles │ │ │ ├── route │ │ │ │ ├── docs-page-index.scss │ │ │ │ └── index.scss │ │ │ ├── hljs │ │ │ │ ├── index.scss │ │ │ │ └── tomorrow.scss │ │ │ ├── app.scss │ │ │ ├── breakpoints.scss │ │ │ ├── remark.scss │ │ │ └── body.scss │ │ ├── helpers │ │ │ └── or.js │ │ ├── routes │ │ │ ├── docs.js │ │ │ ├── docs │ │ │ │ ├── index.js │ │ │ │ └── page.js │ │ │ ├── playground │ │ │ │ ├── messages.js │ │ │ │ ├── auth │ │ │ │ │ └── email.js │ │ │ │ ├── route.js │ │ │ │ └── messages │ │ │ │ │ ├── message │ │ │ │ │ └── index.js │ │ │ │ │ └── message.js │ │ │ ├── index.js │ │ │ └── application.js │ │ ├── app.js │ │ ├── services │ │ │ ├── config.js │ │ │ └── docs.js │ │ ├── instance-initializers │ │ │ └── dummy.js │ │ ├── store.js │ │ ├── util │ │ │ └── array.js │ │ ├── index.html │ │ └── router.js │ ├── public │ │ └── robots.txt │ └── config │ │ ├── optional-features.json │ │ ├── targets.js │ │ ├── ember-cli-update.json │ │ └── environment.js ├── helpers │ ├── setup-helpers.js │ ├── setup.js │ └── util.js ├── test-helper.js ├── unit │ ├── auth-methods-popup-test.js │ ├── auth-methods-anonymos-test.js │ ├── setup-test.js │ ├── models-classic-test.js │ ├── storage-test.js │ ├── functions-region-test.js │ ├── functions-test.js │ └── auth-test.js ├── index.html └── integration │ └── components-stats-test.js ├── .watchmanconfig ├── .prettierrc.js ├── addon ├── store.js ├── user.js ├── -private │ ├── factory │ │ ├── factory │ │ │ ├── models.js │ │ │ ├── zuglet.js │ │ │ └── -base.js │ │ └── get-factory.js │ ├── model │ │ ├── state │ │ │ ├── model.js │ │ │ ├── root.js │ │ │ ├── activators.js │ │ │ └── index.js │ │ ├── decorators │ │ │ ├── root.js │ │ │ ├── cached.js │ │ │ └── route.js │ │ ├── properties │ │ │ ├── property │ │ │ │ ├── index.js │ │ │ │ ├── decorator.js │ │ │ │ └── property.js │ │ │ ├── activate │ │ │ │ ├── writable.js │ │ │ │ ├── content.js │ │ │ │ ├── activators │ │ │ │ │ └── object.js │ │ │ │ └── activate.js │ │ │ └── activate.js │ │ └── tracking │ │ │ ├── tag.js │ │ │ └── utils.js │ ├── util │ │ ├── delay.js │ │ ├── runloop.js │ │ ├── snapshot.js │ │ ├── to-json.js │ │ ├── to-string.js │ │ ├── model-factory.js │ │ ├── array.js │ │ ├── to-primitive.js │ │ ├── date.js │ │ ├── alive.js │ │ ├── set-global.js │ │ ├── random-string.js │ │ ├── get-owner.js │ │ ├── activate.js │ │ ├── fastboot.js │ │ ├── diff-arrays.js │ │ ├── listeners.js │ │ ├── resolve.js │ │ ├── types.js │ │ ├── error.js │ │ └── object-to-json.js │ ├── stores │ │ └── get-stores.js │ ├── store │ │ ├── auth │ │ │ ├── methods │ │ │ │ ├── method.js │ │ │ │ ├── popup.js │ │ │ │ ├── anonymous.js │ │ │ │ ├── token.js │ │ │ │ ├── popup │ │ │ │ │ └── google.js │ │ │ │ └── email.js │ │ │ └── methods.js │ │ ├── firebase.js │ │ ├── firestore │ │ │ ├── references │ │ │ │ ├── condition.js │ │ │ │ ├── collection-group.js │ │ │ │ ├── reference.js │ │ │ │ └── collection.js │ │ │ ├── transaction.js │ │ │ ├── query │ │ │ │ └── single.js │ │ │ └── batch.js │ │ ├── functions │ │ │ └── region.js │ │ └── storage │ │ │ └── storage.js │ └── object.js ├── components │ └── zuglet │ │ ├── stalled.hbs │ │ ├── stats.js │ │ ├── stalled.js │ │ └── stats.hbs ├── decorators │ └── object.js ├── object.js ├── decorators.js └── utils.js ├── app ├── components │ └── zuglet │ │ ├── stats.js │ │ └── stalled.js ├── instance-initializers │ └── zuglet-fastboot.js └── initializers │ └── zuglet-version.js ├── server ├── .eslintrc.js └── index.js ├── config ├── environment.js └── ember-try.js ├── jsconfig.json ├── .template-lintrc.js ├── blueprints └── ember-cli-zuglet │ ├── index.js │ └── files │ └── __root__ │ ├── models │ └── user.js │ ├── routes │ ├── application.js │ └── index.js │ ├── instance-initializers │ └── __name__-store.js │ ├── templates │ └── index.hbs │ └── store.js ├── .ember-cli ├── .editorconfig ├── .prettierignore ├── .eslintignore ├── .gitignore ├── testem.js ├── vendor └── zuglet │ └── fastboot.js ├── .npmignore ├── LICENSE.md ├── index.js ├── ember-cli-build.js ├── config.js ├── .eslintrc.js ├── .travis.yml └── README.md /docs/index/why.md: -------------------------------------------------------------------------------- 1 | • 2 | -------------------------------------------------------------------------------- /firebase/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ampatspell] 2 | -------------------------------------------------------------------------------- /firebase/standalone/.gitgnore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/index-loading.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/page-loading.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/guides.md: -------------------------------------------------------------------------------- 1 | --- 2 | pos: 2 3 | --- 4 | 5 | # Guides 6 | -------------------------------------------------------------------------------- /docs/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | pos: 1 3 | --- 4 | 5 | # TODO 6 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/master-detail/section.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | hidden: true 3 | --- 4 | 5 | # Documentation 6 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/auth.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/dev.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/json.hbs: -------------------------------------------------------------------------------- 1 |
{{this.string}}
-------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/content.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/models.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/storage.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/missing.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/document.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/functions.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/reordering.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/query/array.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/query/single.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /addon/store.js: -------------------------------------------------------------------------------- 1 | import Store from './-private/store/store'; 2 | 3 | export default Store; 4 | -------------------------------------------------------------------------------- /app/components/zuglet/stats.js: -------------------------------------------------------------------------------- 1 | export { default } from 'zuglet/components/zuglet/stats'; 2 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "ember-cli-zuglet"}} 2 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /addon/user.js: -------------------------------------------------------------------------------- 1 | import User from './-private/store/auth/user'; 2 | 3 | export default User; 4 | -------------------------------------------------------------------------------- /app/components/zuglet/stalled.js: -------------------------------------------------------------------------------- 1 | export { default } from 'zuglet/components/zuglet/stalled'; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/route.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /tests/dummy/app/templates/playground.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /addon/-private/factory/factory/models.js: -------------------------------------------------------------------------------- 1 | import make from './-base'; 2 | 3 | export default make('model'); 4 | -------------------------------------------------------------------------------- /addon/-private/factory/factory/zuglet.js: -------------------------------------------------------------------------------- 1 | import make from './-base'; 2 | 3 | export default make('zuglet'); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/page/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/messages/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /firebase/README.md: -------------------------------------------------------------------------------- 1 | ## cors 2 | 3 | ``` bash 4 | $ gsutil cors set cors.json gs:// 5 | ``` 6 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/docs/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /addon/-private/model/state/model.js: -------------------------------------------------------------------------------- 1 | import State from './state'; 2 | 3 | export default class ModelState extends State { 4 | } 5 | -------------------------------------------------------------------------------- /addon/-private/util/delay.js: -------------------------------------------------------------------------------- 1 | export const delay = (delay=1000) => new Promise(resolve => setTimeout(() => resolve(), delay)); 2 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /firebase/cors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "origin": [ "*" ], 4 | "method": [ "GET" ], 5 | "maxAgeSeconds": 3600 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/stalled.hbs: -------------------------------------------------------------------------------- 1 |
2 |
Still working…
3 |
-------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/messages/message/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/changes/property.hbs: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/block/remark/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /addon/-private/model/decorators/root.js: -------------------------------------------------------------------------------- 1 | import { setRoot } from '../state'; 2 | 3 | export const root = () => Class => setRoot(Class); 4 | -------------------------------------------------------------------------------- /firebase/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ember-cli-zuglet", 4 | "travis": "ember-cli-zuglet-travis" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/fancy-message.js: -------------------------------------------------------------------------------- 1 | import Message from './message'; 2 | 3 | export default class FancyMessage extends Message { 4 | } 5 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/route/docs-page-index.scss: -------------------------------------------------------------------------------- 1 | .route-docs-page-index { 2 | > .placeholder { 3 | padding: 15px 0; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /addon/components/zuglet/stalled.hbs: -------------------------------------------------------------------------------- 1 | {{#let this.stalledPromises as |stats|}} 2 | {{#if stats}} 3 | {{yield stats}} 4 | {{/if}} 5 | {{/let}} -------------------------------------------------------------------------------- /docs/api/auth/methods/popup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Popup 3 | pos: 3 4 | --- 5 | 6 | # Popup 7 | 8 | ## google 9 | 10 | Google accounts 11 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/remark/link-to.hbs: -------------------------------------------------------------------------------- 1 | {{@content}} -------------------------------------------------------------------------------- /tests/dummy/app/templates/playground/messages.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /addon/-private/util/runloop.js: -------------------------------------------------------------------------------- 1 | import { next as _next } from '@ember/runloop'; 2 | 3 | export const next = () => new Promise(resolve => _next(resolve)); 4 | -------------------------------------------------------------------------------- /addon/decorators/object.js: -------------------------------------------------------------------------------- 1 | import { object, raw, update } from '../-private/model/properties/object'; 2 | 3 | export { 4 | object, 5 | raw, 6 | update 7 | }; 8 | -------------------------------------------------------------------------------- /addon/object.js: -------------------------------------------------------------------------------- 1 | import ZugletObject, { setProperties } from './-private/object'; 2 | 3 | export { 4 | setProperties 5 | }; 6 | 7 | export default ZugletObject; 8 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/or.js: -------------------------------------------------------------------------------- 1 | import { helper } from '@ember/component/helper'; 2 | 3 | export default helper(function or([ a, b ]) { 4 | return a || b; 5 | }); 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "experimentalDecorators": true 5 | }, 6 | "exclude": [ "**/node_modules/*", ".git" ] 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/factory/get-factory.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '../util/get-owner'; 2 | 3 | export const getFactory = (owner, opts) => getOwner(owner, opts)?.lookup('zuglet:factory'); 4 | -------------------------------------------------------------------------------- /addon/-private/stores/get-stores.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '../util/get-owner'; 2 | 3 | export const getStores = (owner, opts) => getOwner(owner, opts)?.lookup('zuglet:stores'); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/messages/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{@model}} 5 |
6 | 7 |
-------------------------------------------------------------------------------- /firebase/standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "standalone", 3 | "private": true, 4 | "dependencies": { 5 | "firebase-admin": "^10.0.1", 6 | "minimist": "^1.2.6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/toc/page.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{@page.title}} 3 | 4 |
-------------------------------------------------------------------------------- /addon/-private/model/properties/property/index.js: -------------------------------------------------------------------------------- 1 | import Property from './property'; 2 | import { property } from './decorator'; 3 | 4 | export { 5 | property 6 | } 7 | 8 | export default Property; 9 | -------------------------------------------------------------------------------- /addon/-private/util/snapshot.js: -------------------------------------------------------------------------------- 1 | export const snapshotToDeferredType = snapshot => { 2 | let { fromCache } = snapshot.metadata; 3 | if(fromCache) { 4 | return 'cached'; 5 | } 6 | return 'remote'; 7 | } 8 | -------------------------------------------------------------------------------- /addon/-private/util/to-json.js: -------------------------------------------------------------------------------- 1 | import { toPrimitive } from './to-primitive'; 2 | 3 | export const toJSON = (instance, props) => { 4 | return { 5 | instance: toPrimitive(instance), 6 | ...props 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/helpers/setup-helpers.js: -------------------------------------------------------------------------------- 1 | export const setupHelpers = hooks => { 2 | hooks.beforeEach(function() { 3 | this.registerModel = (name, factory) => this.owner.register(`model:${name}`, factory); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/toc/pages.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#each @pages as |nested|}} 3 | {{#unless nested.hidden}} 4 | 5 | {{/unless}} 6 | {{/each}} 7 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/block/index/section.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{@aside}} 4 |
5 |
6 | {{yield}} 7 |
8 |
9 | -------------------------------------------------------------------------------- /docs/index/what.md: -------------------------------------------------------------------------------- 1 | ember-cli-zuglet is an [Ember.js](https://emberjs.com/) Octane Edition addon that lets you build complex apps with [Google Firebase](http://firebase.google.com) Firestore, Auth, Storage and Functions easy and fun. 2 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/docs.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if md.isMaster}} 3 | 4 | {{else if md.isDetail}} 5 | {{yield}} 6 | {{/if}} 7 | 8 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/missing.hbs: -------------------------------------------------------------------------------- 1 |
2 |
Page /{{@path}} was not found
3 |
4 | ← back to index 5 |
6 |
-------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers 11 | }; 12 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | rules: { 6 | 'no-index-component-invocation': false, 7 | 'require-input-label': false, 8 | 'no-yield-only': false 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /docs/api/firestore.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Firestore 3 | pos: 3 4 | --- 5 | 6 | # Firestore 7 | 8 | * [Store](api/store) 9 | * [References](api/firestore/reference) 10 | * [Document](api/firestore/document) 11 | * [Query](api/firestore/query) 12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/master-detail.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{yield (hash isMaster=true)}} 4 |
5 |
6 | {{yield (hash isDetail=true)}} 7 |
8 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/block/toc.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class BlockTocComponent extends Component { 5 | 6 | @service config; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/hljs/index.scss: -------------------------------------------------------------------------------- 1 | @import "tomorrow"; 2 | 3 | .hljs { 4 | font-family: $font-code; 5 | padding: 0; 6 | border: 1px solid fade-out(#000, 0.9); 7 | border-radius: 3px; 8 | padding: 9px 10px 8px 10px; 9 | } 10 | -------------------------------------------------------------------------------- /addon/-private/util/to-string.js: -------------------------------------------------------------------------------- 1 | import { toPrimitive } from './to-primitive'; 2 | 3 | export const toString = (model, string) => { 4 | if(!model) { 5 | return; 6 | } 7 | return `<${toPrimitive(model)}${string ? `:${string}` : ''}>`; 8 | }; 9 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/docs/page/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if @page}} 3 | 4 | {{else}} 5 |
Page "{{@id}}" was not found
6 | {{/if}} 7 |
-------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | normalizeEntityName() { 6 | }, 7 | fileMapTokens() { 8 | return { 9 | __root__: () => '/app' 10 | }; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/files/__root__/models/user.js: -------------------------------------------------------------------------------- 1 | import User from 'zuglet/user'; 2 | 3 | export default class <%= classifiedPackageName %>User extends User { 4 | 5 | async restore(user) { 6 | await super.restore(user); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /docs/api/firestore/reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reference 3 | pos: 0 4 | --- 5 | 6 | # Reference 7 | 8 | * [Document](api/firestore/reference/document) 9 | * [Queryable](api/firestore/reference/queryable) & [Collection](api/firestore/reference/collection) 10 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/index.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class RouteIndexComponent extends Component { 5 | 6 | @service 7 | config 8 | 9 | } 10 | -------------------------------------------------------------------------------- /addon/-private/store/auth/methods/method.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../../object'; 2 | 3 | export default class AuthMethod extends ZugletObject { 4 | 5 | constructor(owner, { auth }) { 6 | super(owner); 7 | this.auth = auth; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /addon/-private/util/model-factory.js: -------------------------------------------------------------------------------- 1 | const MODELS = new WeakMap(); 2 | 3 | export const registerModel = (model, modelName) => { 4 | MODELS.set(model, { modelName }); 5 | return model; 6 | }; 7 | 8 | export const getModelName = model => MODELS.get(model)?.modelName; 9 | -------------------------------------------------------------------------------- /tests/dummy/app/models/user.js: -------------------------------------------------------------------------------- 1 | import User from 'zuglet/user'; 2 | 3 | export default class DummyUser extends User { 4 | 5 | async restore(user /*, details*/) { 6 | await super.restore(user); 7 | // console.log(this.uid, details); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | let express = require('express'); // eslint-disable-line node/no-extraneous-require 5 | let path = require('path'); 6 | app.use('/coverage', express.static(path.join(__dirname, '..', 'coverage'))); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/docs.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class DocsRoute extends Route { 5 | 6 | @service docs; 7 | 8 | model() { 9 | return this.docs; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /firebase/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | 3 | module.exports.echo = functions.https.onCall((data, context) => { 4 | let { auth } = context; 5 | let uid; 6 | if(auth) { 7 | uid = auth.uid; 8 | } 9 | return { data, uid }; 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/query/single.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
-------------------------------------------------------------------------------- /docs/api/auth/methods.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Methods 3 | pos: 1 4 | --- 5 | 6 | # Methods 7 | 8 | ## anonymous 9 | 10 | anonymous accounts 11 | 12 | ## email 13 | 14 | email-based accounts 15 | 16 | ## token 17 | 18 | sign in with custom token 19 | 20 | ## popup 21 | 22 | popup-based sign-in 23 | -------------------------------------------------------------------------------- /docs/api/auth/methods/anonymous.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Anonymous 3 | pos: 0 4 | --- 5 | 6 | # Anonymous 7 | 8 | ## async signIn() `→ User` 9 | 10 | Sign-in anonymously 11 | 12 | ``` javascript 13 | let user = await store.auth.methods.anonymous.signIn(); 14 | store.auth.user === user // → true 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/api/auth/methods/token.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Token 3 | pos: 2 4 | --- 5 | 6 | # Token 7 | 8 | ## async signIn(token) `→ User` 9 | 10 | Sign-in with custom token 11 | 12 | ``` javascript 13 | let user = await store.auth.methods.token.signIn(token); 14 | store.auth.user === user // → true 15 | ``` 16 | -------------------------------------------------------------------------------- /addon/-private/util/array.js: -------------------------------------------------------------------------------- 1 | export const removeAt = (array, index) => { 2 | if(index > -1) { 3 | array.splice(index, 1); 4 | } 5 | return array; 6 | } 7 | 8 | export const removeObject = (array, object) => { 9 | let index = array.indexOf(object); 10 | return removeAt(array, index); 11 | } 12 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | pos: 4 4 | --- 5 | 6 | # API Documentation 7 | 8 | ## Exports 9 | 10 | ``` javascript 11 | import { initialize } from 'zuglet/initialize'; 12 | import ZugletObject from 'zuglet/object'; 13 | import Store from 'zuglet/store'; 14 | import User from 'zuglet/user'; 15 | ``` 16 | -------------------------------------------------------------------------------- /tests/dummy/app/components/json.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { objectToJSON } from 'zuglet/utils'; 3 | 4 | export default class JsonComponent extends Component { 5 | 6 | get string() { 7 | return JSON.stringify(objectToJSON(this.args.value), null, 2); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/docs/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class DocsIndexRoute extends Route { 5 | 6 | @service docs; 7 | 8 | model() { 9 | return this.docs.page('docs').load(); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/route.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | 4 | export default class RouteRouteComponent extends Component { 5 | 6 | @action 7 | async add() { 8 | await this.args.model.add('Untitled'); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /firebase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebase", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "emulators": "firebase emulators:start --import ./export --export-on-exit ./export" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/docs/route.hbs: -------------------------------------------------------------------------------- 1 | {{~#if @node.properties.model~}} 2 | 3 | {{~else~}} 4 | 5 | {{~/if~}} -------------------------------------------------------------------------------- /tests/dummy/app/components/input/file.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if this.file}} 3 |
{{this.file.name}} ({{this.size}})
4 | {{else}} 5 |
Choose file…
6 | {{/if}} 7 | 8 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/dev.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{this}} 5 |
6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/route.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 | {{#each @model.models as |model|}} 8 | 9 | {{/each}} 10 | 11 |
-------------------------------------------------------------------------------- /docs/api/firestore/reference/condition.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Condition 3 | pos: 5 4 | --- 5 | 6 | # Condition Reference `extends QueryableReference` 7 | 8 | See [Queryable Reference](api/firestore/reference/queryable) for query `load`, `query` and condition methods which are shared between `Collection`, `CollectionGroup` and `Condition` references. 9 | -------------------------------------------------------------------------------- /addon/-private/util/to-primitive.js: -------------------------------------------------------------------------------- 1 | import { guidFor } from '@ember/object/internals'; 2 | import { getModelName } from './model-factory'; 3 | 4 | export const toPrimitive = model => { 5 | if(!model) { 6 | return; 7 | } 8 | let name = getModelName(model) || model.constructor.name; 9 | return `${name}::${guidFor(model)}`; 10 | }; 11 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/messages/message/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{@model}} 5 |
6 | 7 |
8 | {{@model.message}} 9 |
10 | 11 |
12 | {{@model.message.id}} 13 |
14 | 15 |
-------------------------------------------------------------------------------- /addon/-private/util/date.js: -------------------------------------------------------------------------------- 1 | let _dateTimeFormatter = new Intl.DateTimeFormat('default', { 2 | year: 'numeric', 3 | month: 'numeric', 4 | day: 'numeric', 5 | hour: 'numeric', 6 | minute: 'numeric', 7 | seconds: 'numeric', 8 | timeZoneName: 'short' 9 | }); 10 | 11 | export const formatDate = value => _dateTimeFormatter.format(value); 12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/changes/property.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | 4 | export default class BlockChangesPropertyComponent extends Component { 5 | 6 | @action 7 | onValue(_, [ value ]) { 8 | this.args.onPropertyChange(this.args.key, value); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /docs/decorators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Decorators 3 | pos: 3 4 | --- 5 | 6 | # Decorators 7 | 8 | Decorators are used to compose overall architecture for apps built using `ember-cli-zuglet`. 9 | 10 | ## Exports 11 | 12 | ``` javascript 13 | import { 14 | route, 15 | root, 16 | activate, 17 | model, 18 | models 19 | } from 'zuglet/decorators'; 20 | ``` 21 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{yield}} 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /addon/-private/util/alive.js: -------------------------------------------------------------------------------- 1 | import { isDestroying } from '@ember/destroyable'; 2 | 3 | export const alive = () => (target, key, descriptor) => { 4 | let fn = descriptor.value; 5 | return { 6 | value: function(...args) { 7 | if(isDestroying(this)) { 8 | return; 9 | } 10 | return fn.call(this, ...args); 11 | } 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/document.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/block/changes.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{this.log}}
3 | {{#unless this.log}} 4 |
No events
5 | {{/unless}} 6 | {{yield (hash 7 | Property=(component 'block/changes/property' onPropertyChange=this.onPropertyChange) 8 | )}} 9 |
-------------------------------------------------------------------------------- /tests/dummy/app/models/pages/messages/message/index.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from 'zuglet/object'; 2 | 3 | export default class PagesMessagesMessageIndex extends ZugletObject { 4 | 5 | constructor(owner, { message }) { 6 | super(owner); 7 | this.message = message; 8 | } 9 | 10 | async load() { 11 | return await this.message.load(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /docs/api/auth/methods/popup/google.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Google 3 | pos: 0 4 | --- 5 | 6 | # Google 7 | 8 | ## async signIn(scopes) `→ User` 9 | 10 | Sign-in using Google account. 11 | 12 | * `scopes` → defaults to `[ 'profile', 'email' ]` 13 | 14 | ``` javascript 15 | let user = await store.auth.methods.popup.google.signIn(); 16 | store.auth.user === user // → true 17 | ``` 18 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/message.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | 4 | export default class BlockMessageComponent extends Component { 5 | 6 | @action 7 | async save() { 8 | await this.args.message.save(); 9 | } 10 | 11 | @action 12 | async delete() { 13 | await this.args.message.delete(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/query/array.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 | {{#each this.query.content as |doc|}} 8 |
9 | 10 |
11 | {{else}} 12 |
Nothing here yet
13 | {{/each}} 14 | 15 |
-------------------------------------------------------------------------------- /addon/-private/store/auth/methods/popup.js: -------------------------------------------------------------------------------- 1 | import AuthMethod from './method'; 2 | import { cached } from '../../../model/decorators/cached'; 3 | 4 | export default class AuthPopupMethod extends AuthMethod { 5 | 6 | _method(name) { 7 | return this.auth.methods._method(`popup/${name}`); 8 | } 9 | 10 | @cached() 11 | get google() { 12 | return this._method('google'); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/functions.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 | 15 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/content.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | {{this.query.string}} 6 |
7 |
8 | 9 |
10 |
11 | 12 | {{#each this.models as |model|}} 13 | 14 | {{/each}} 15 | 16 |
-------------------------------------------------------------------------------- /addon/-private/util/set-global.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | export const setGlobal = (hash, silent=false) => { 3 | if(typeof window === 'undefined') { 4 | return; 5 | } 6 | for(let key in hash) { 7 | let value = hash[key]; 8 | if(!silent) { 9 | // eslint-disable-next-line no-console 10 | console.log(`window.${key} = ${value}`); 11 | } 12 | window[key] = value; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700'); 2 | 3 | $font-code: 'Ubuntu Mono'; 4 | $font-text: 'Ubuntu Mono'; 5 | $font-size: 14px; 6 | $font-size-smaller: 12px; 7 | 8 | @import "body"; 9 | @import "breakpoints"; 10 | @import "application"; 11 | @import "playground"; 12 | @import "remark"; 13 | @import "route/index"; 14 | @import "route/docs-page-index"; 15 | -------------------------------------------------------------------------------- /app/instance-initializers/zuglet-fastboot.js: -------------------------------------------------------------------------------- 1 | import { lookupFastBoot } from 'zuglet/-private/util/fastboot'; 2 | 3 | export default { 4 | name: 'zuglet:fastboot', 5 | initialize(app) { 6 | let { fastboot, isFastBoot } = lookupFastBoot(app); 7 | /* istanbul ignore next */ 8 | if(isFastBoot) { 9 | let stores = app.lookup('zuglet:stores'); 10 | fastboot.deferRendering(stores.settle()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /firebase/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | 5 | match /hello { 6 | allow read: if true; 7 | allow write: if true; 8 | } 9 | 10 | match /files/{file} { 11 | allow read: if true; 12 | allow write: if true; 13 | match /{nested} { 14 | allow read: if true; 15 | allow write: if true; 16 | } 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /addon/-private/util/random-string.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript 2 | 3 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | 5 | export const randomString = (len=8) => { 6 | let string = ''; 7 | for(let i = 0; i < len; i++) { 8 | string += possible.charAt(Math.floor(Math.random() * possible.length)); 9 | } 10 | return string; 11 | }; 12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/toc.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{this.config.name}} v{{this.config.version}} 4 |
Firebase v{{this.config.firebase}}
5 | Documentation 6 |
7 | 8 |
-------------------------------------------------------------------------------- /addon/-private/store/auth/methods/anonymous.js: -------------------------------------------------------------------------------- 1 | import AuthMethod from './method'; 2 | import { registerPromise } from '../../../stores/stats'; 3 | 4 | export default class AnonymousAuthMethod extends AuthMethod { 5 | 6 | signIn() { 7 | return this.auth._withAuthReturningUser(async auth => { 8 | let { user } = await registerPromise(this, 'sign-in', true, auth.signInAnonymously()); 9 | return { user }; 10 | }); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /addon/-private/store/auth/methods/token.js: -------------------------------------------------------------------------------- 1 | import AuthMethod from './method'; 2 | import { registerPromise } from '../../../stores/stats'; 3 | 4 | export default class TokenAuthMethod extends AuthMethod { 5 | 6 | signIn(token) { 7 | return this.auth._withAuthReturningUser(async auth => { 8 | let { user } = await registerPromise(this, 'sign-in', true, auth.signInWithCustomToken(token)); 9 | return { user }; 10 | }); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/playground/messages.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | 5 | @route() 6 | export default class MessagesRoute extends Route { 7 | 8 | @service 9 | store 10 | 11 | model() { 12 | return this.store.models.create('messages'); 13 | } 14 | 15 | async load(model) { 16 | await model.load(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | .lint-todo/ 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /npm-shrinkwrap.json.ember-try 23 | /package.json.ember-try 24 | /package-lock.json.ember-try 25 | /yarn.lock.ember-try 26 | -------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/files/__root__/routes/application.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | import { load } from 'zuglet/utils'; 5 | 6 | @route() 7 | export default class ApplicationRoute extends Route { 8 | 9 | @service 10 | store 11 | 12 | model() { 13 | } 14 | 15 | async load() { 16 | await load(this.store.auth); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "4.2.0", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /addon/-private/model/state/root.js: -------------------------------------------------------------------------------- 1 | import State from './state'; 2 | import { registerDestructor } from '@ember/destroyable'; 3 | 4 | export default class RootState extends State { 5 | 6 | constructor() { 7 | super(...arguments); 8 | registerDestructor(this.owner, () => this._onOwnerWillDestroy()); 9 | } 10 | 11 | didCreateState() { 12 | this.activate(this); 13 | } 14 | 15 | _onOwnerWillDestroy() { 16 | this.deactivate(this); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/docs/page.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class DocsPageRoute extends Route { 5 | 6 | @service docs; 7 | 8 | async model({ page_id: id }) { 9 | if(id === 'index') { 10 | return this.transitionTo('index'); 11 | } 12 | let page = await this.docs.page(id)?.load(); 13 | return { 14 | id, 15 | page 16 | }; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /addon/-private/store/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/compat/app"; 2 | 3 | let id = 0; 4 | 5 | export const initializeApp = (config, name) => firebase.initializeApp(config, `${name}-${id++}`); 6 | export const destroyApp = app => app.delete(); 7 | 8 | export const enablePersistence = async firestore => { 9 | await firestore.enablePersistence({ synchronizeTabs: true }).catch(err => { 10 | console.error('firestore/enable-persistence', err.stack); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /addon/-private/util/get-owner.js: -------------------------------------------------------------------------------- 1 | import { getOwner as emberGetOwner } from '@ember/application'; 2 | import { assert } from '@ember/debug'; 3 | 4 | const { 5 | assign 6 | } = Object; 7 | 8 | export const getOwner = (object, opts) => { 9 | let { optional } = assign({ optional: false }, opts); 10 | assert(`object is required`, !!object); 11 | let owner = emberGetOwner(object); 12 | assert(`${object} must have Ember.js owner`, !!owner || optional); 13 | return owner; 14 | }; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/models/post.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from 'zuglet/object'; 2 | import { reads } from 'macro-decorators'; 3 | 4 | const data = key => reads(`doc.data.${key}`); 5 | 6 | export default class Post extends ZugletObject { 7 | 8 | constructor(owner, { doc }) { 9 | super(owner); 10 | this.doc = doc; 11 | } 12 | 13 | @data('title') title; 14 | @data('position') position; 15 | 16 | load(type) { 17 | console.log('load', type, this+''); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /docs/api/firestore/reference/collection-group.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collection Group 3 | pos: 4 4 | --- 5 | 6 | # Collection Group Reference `extends QueryableReference` 7 | 8 | See [Queryable Reference](api/firestore/reference/queryable) for query `load`, `query` and condition methods which are shared between `Collection`, `CollectionGroup` and `Condition` references. 9 | 10 | ``` javascript 11 | let ref = store.group('messages'); 12 | ``` 13 | 14 | ## id `→ string` 15 | 16 | Collection id 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /npm-shrinkwrap.json.ember-try 23 | /package.json.ember-try 24 | /package-lock.json.ember-try 25 | /yarn.lock.ember-try 26 | 27 | /firebase 28 | /jsconfig.json 29 | -------------------------------------------------------------------------------- /addon/-private/model/properties/property/decorator.js: -------------------------------------------------------------------------------- 1 | import { getState } from '../../state'; 2 | import { getFactory } from '../../../factory/get-factory'; 3 | 4 | const createProperty = (state, owner, name, key, opts) => { 5 | return getFactory(owner).zuglet.create(`properties/${name}`, { state, owner, key, opts }); 6 | } 7 | 8 | export const property = (owner, key, name, opts) => { 9 | return getState(owner).getProperty(key, state => createProperty(state, owner, name, key, opts)); 10 | } 11 | -------------------------------------------------------------------------------- /addon/-private/util/activate.js: -------------------------------------------------------------------------------- 1 | import { getState } from '../model/state'; 2 | 3 | class Activator { 4 | toString() { 5 | return ``; 6 | } 7 | } 8 | 9 | let _activator = new Activator(); 10 | 11 | export const activate = (model, activator=_activator) => { 12 | let state = getState(model); 13 | state.activate(activator); 14 | return () => { 15 | state.deactivate(activator); 16 | } 17 | }; 18 | 19 | export const isActivated = model => getState(model).isActivated; 20 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/models.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | {{#each this.models as |model|}} 13 | 14 | {{/each}} 15 | 16 |
-------------------------------------------------------------------------------- /tests/dummy/app/services/config.js: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | import environment from '../config/environment'; 3 | import flags from 'zuglet/-private/flags'; 4 | 5 | let { 6 | version: { 7 | zuglet: version, 8 | firebase 9 | } 10 | } = flags; 11 | 12 | let { 13 | dummy: { 14 | name 15 | } 16 | } = environment; 17 | 18 | export default class ConfigService extends Service { 19 | 20 | name = name; 21 | version = version; 22 | firebase = firebase; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /addon/decorators.js: -------------------------------------------------------------------------------- 1 | import { root } from './-private/model/decorators/root'; 2 | import { route } from './-private/model/decorators/route'; 3 | import { activate } from './-private/model/properties/activate'; 4 | import { object } from './-private/model/properties/object'; 5 | import { models } from './-private/model/properties/models'; 6 | import { model } from './-private/model/properties/model'; 7 | 8 | export { 9 | root, 10 | route, 11 | activate, 12 | object, 13 | models, 14 | model 15 | }; 16 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { hash } from 'rsvp'; 4 | 5 | export default class IndexRoute extends Route { 6 | 7 | @service 8 | docs 9 | 10 | async model() { 11 | let pages = await hash(this.docs.directory('index').reduce((hash, page) => { 12 | hash[page.name] = page.load(); 13 | return hash; 14 | }, {})); 15 | return { 16 | pages 17 | }; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/playground/auth/email.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | 4 | export default class MessagesRoute extends Route { 5 | 6 | @service store; 7 | @service router; 8 | 9 | async model({ email }) { 10 | try { 11 | await this.store.auth.methods.email.signInWithLink(email); 12 | } catch(err) { 13 | console.log(err); 14 | } 15 | this.router.transitionTo('playground.auth'); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /addon/-private/util/fastboot.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '@ember/application'; 2 | 3 | export const lookupFastBoot = owner => { 4 | let fastboot = owner.lookup('service:fastboot'); 5 | let isFastBoot = fastboot && fastboot.isFastBoot; 6 | return { 7 | fastboot, 8 | isFastBoot 9 | }; 10 | }; 11 | 12 | export const getFastBoot = sender => lookupFastBoot(getOwner(sender)); 13 | 14 | export const isFastBoot = sender => { 15 | let { isFastBoot } = getFastBoot(sender); 16 | return isFastBoot; 17 | }; 18 | -------------------------------------------------------------------------------- /docs/api/functions/region.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Region 3 | pos: 0 4 | --- 5 | 6 | # Region 7 | 8 | ## identifier `→ string` 9 | 10 | Region identifier 11 | 12 | ``` javascript 13 | let region = store.functions.region(); 14 | region.identifier // → 'us-central1' 15 | ``` 16 | 17 | ## async call(name, props) `→ object` 18 | 19 | Calls Firebase callable function. 20 | 21 | ``` javascript 22 | let region = store.functions.region(); 23 | let result = await region.call('hello', { ok: true }); 24 | result // → { data: { … } } 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | 5 | @route() 6 | export default class RouteRoute extends Route { 7 | 8 | @service store; 9 | @service docs; 10 | 11 | model() { 12 | } 13 | 14 | async load() { 15 | // resolve current user before rendering app 16 | await Promise.all([ 17 | this.store.load(), 18 | this.docs.load() 19 | ]); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/references/condition.js: -------------------------------------------------------------------------------- 1 | import QueryableReference from './queryable'; 2 | 3 | export default class ConditionReference extends QueryableReference { 4 | 5 | constructor(owner, opts) { 6 | super(owner, opts); 7 | let { string, parent } = opts; 8 | this.string = string; 9 | this.parent = parent; 10 | } 11 | 12 | get serialized() { 13 | let { string } = this; 14 | return { string }; 15 | } 16 | 17 | toStringExtension() { 18 | return `${this.string}`; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/initializers/zuglet-version.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import flags from 'zuglet/-private/flags'; 3 | 4 | const { 5 | libraries 6 | } = Ember; 7 | 8 | const { 9 | version 10 | } = flags; 11 | 12 | let registered = false; 13 | 14 | export default { 15 | name: 'zuglet:version', 16 | initialize() { 17 | if(registered) { 18 | return; 19 | } 20 | registered = true; 21 | libraries.register('ember-cli-zuglet', version.zuglet); 22 | libraries.register('firebase', version.firebase); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/unit/auth-methods-popup-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | 3 | module('auth / methods / popup', function(hooks) { 4 | setupStoreTest(hooks); 5 | 6 | hooks.beforeEach(async function() { 7 | this.auth = this.store.auth; 8 | }); 9 | 10 | test('popup exists', async function(assert) { 11 | assert.ok(this.auth.methods.popup); 12 | }); 13 | 14 | test('popup.google exists', async function(assert) { 15 | assert.ok(this.auth.methods.popup.google); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/dev.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | import { setGlobal, toString } from 'zuglet/utils'; 4 | import { root } from 'zuglet/decorators'; 5 | 6 | @root() 7 | export default class RouteDevComponent extends Component { 8 | 9 | @service 10 | store 11 | 12 | constructor() { 13 | super(...arguments); 14 | setGlobal({ component: this }); 15 | } 16 | 17 | toString() { 18 | return toString(this); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/index.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | import { setGlobal, toString } from 'zuglet/utils'; 4 | import { root } from 'zuglet/decorators'; 5 | 6 | @root() 7 | export default class RouteIndexComponent extends Component { 8 | 9 | @service 10 | store 11 | 12 | constructor() { 13 | super(...arguments); 14 | setGlobal({ component: this }); 15 | } 16 | 17 | toString() { 18 | return toString(this); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /firebase/standalone/lib/create-token.js: -------------------------------------------------------------------------------- 1 | let { withContext, argv, exit } = require('./setup'); 2 | 3 | let { project, uid } = argv; 4 | if(!project) { 5 | exit('--project= is required'); 6 | } 7 | 8 | if(!uid) { 9 | exit('--uid= is required'); 10 | } 11 | 12 | withContext(project, async ctx => { 13 | 14 | console.log('projectId:', ctx.config.firebase.projectId); 15 | console.log('uid:', uid); 16 | 17 | let token = await ctx.auth.createCustomToken(uid); 18 | console.log(); 19 | console.log(token); 20 | console.log(); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /docs/api/utils.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Utils 3 | pos: 9 4 | --- 5 | 6 | # Utils 7 | 8 | ``` javascript 9 | import { 10 | getStores, 11 | setGlobal, 12 | objectToJSON, 13 | toString, 14 | toPrimitive, 15 | toJSON, 16 | isZugletError, 17 | load, 18 | alive, 19 | delay, 20 | activate, 21 | isActivated, 22 | defer 23 | } from 'zuglet/utils'; 24 | ``` 25 | 26 | ## load 27 | 28 | ``` js 29 | import { load } from 'zuglet/utils'; 30 | 31 | await load.cached(this.doc); 32 | await load.remote(this.doc); 33 | 34 | await load(this.store.auth); 35 | ``` 36 | -------------------------------------------------------------------------------- /addon/-private/util/diff-arrays.js: -------------------------------------------------------------------------------- 1 | import { A } from '@ember/array'; 2 | 3 | export const diffArrays = (current, next) => { 4 | current = A(current); 5 | next = A(next); 6 | 7 | let added = A(); 8 | let removed = A([ ...current ]); 9 | let intact = A(); 10 | 11 | next.forEach(model => { 12 | if(current.includes(model)) { 13 | intact.pushObject(model); 14 | } else { 15 | added.pushObject(model); 16 | } 17 | removed.removeObject(model); 18 | }); 19 | 20 | return { 21 | added, 22 | removed, 23 | intact 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/files/__root__/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | 5 | @route() 6 | export default class IndexRoute extends Route { 7 | 8 | @service 9 | store 10 | 11 | async model() { 12 | return this.store.doc('messages/first').new({ 13 | message: 'To whom it may concern: It is springtime. It is late afternoon.' 14 | }); 15 | } 16 | 17 | async load(doc) { 18 | return await doc.save(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/references/collection-group.js: -------------------------------------------------------------------------------- 1 | import QueryableReference from './queryable'; 2 | 3 | export default class CollectionGroupReference extends QueryableReference { 4 | 5 | constructor(owner, opts) { 6 | super(owner, opts); 7 | let { id } = opts; 8 | this.id = id; 9 | } 10 | 11 | get string() { 12 | let { id } = this; 13 | return `group(${id})`; 14 | } 15 | 16 | get serialized() { 17 | let { id } = this; 18 | return { id }; 19 | } 20 | 21 | toStringExtension() { 22 | return `${this.id}`; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /docs/index/install.md: -------------------------------------------------------------------------------- 1 | ``` bash 2 | $ ember install ember-cli-zuglet 3 | ``` 4 | 5 | and provide your Firebase project configuration in `app/store.js`. 6 | 7 | You might also want to remove `ember-data` from `package.json` and enable experimental decorators in `jsconfig.json` 8 | 9 | ``` diff 10 | "devDependencies": { 11 | - "ember-data": "~3.22.0", 12 | } 13 | ``` 14 | 15 | ``` javascript 16 | // jsconfig.json 17 | { 18 | "compilerOptions": { 19 | "target": "es6", 20 | "experimentalDecorators": true 21 | }, 22 | "exclude": [ "node_modules", ".git" ] 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/message.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{@message}}
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 |
-------------------------------------------------------------------------------- /tests/dummy/app/models/pages/messages/message.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from 'zuglet/object'; 2 | import { activate } from 'zuglet/decorators'; 3 | 4 | export default class PagesMessagesMessage extends ZugletObject { 5 | 6 | @activate().content(({ messages, id }) => messages.byId(id)) 7 | message; 8 | 9 | constructor(owner, { messages, id }) { 10 | super(owner); 11 | this.messages = messages; 12 | this.id = id; 13 | } 14 | 15 | async load() { 16 | let { message } = this; 17 | if(message) { 18 | await message.load(); 19 | return true; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /npm-shrinkwrap.json.ember-try 27 | /package.json.ember-try 28 | /package-lock.json.ember-try 29 | /yarn.lock.ember-try 30 | 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /tests/dummy/app/instance-initializers/dummy.js: -------------------------------------------------------------------------------- 1 | import { initialize } from 'zuglet/initialize'; 2 | import Store from '../store'; 3 | import { registerDeprecationHandler } from '@ember/debug'; 4 | 5 | export default { 6 | name: 'dummy:dev', 7 | initialize(app) { 8 | registerDeprecationHandler((message, options, next) => { 9 | if(options.id === 'manager-capabilities.modifiers-3-13') { 10 | return; 11 | } 12 | next(message, options, next); 13 | }); 14 | initialize(app, { 15 | store: { 16 | identifier: 'store', 17 | factory: Store 18 | } 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /addon/-private/factory/factory/-base.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../object'; 2 | 3 | const factoryForPrefix = prefix => (_, key) => ({ 4 | value(...args) { 5 | let { factory } = this; 6 | return factory[key].call(factory, prefix, ...args); 7 | } 8 | }); 9 | 10 | export default prefix => { 11 | 12 | const factory = factoryForPrefix(prefix); 13 | 14 | return class Factory extends ZugletObject { 15 | 16 | @factory registerFactory 17 | @factory factoryFor 18 | @factory create 19 | 20 | constructor(owner, { factory }) { 21 | super(owner); 22 | this.factory = factory; 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/files/__root__/instance-initializers/__name__-store.js: -------------------------------------------------------------------------------- 1 | import { initialize } from 'zuglet/initialize'; 2 | import Store from '../store'; 3 | 4 | export default { 5 | name: '<%= dasherizedPackageName %>:store', 6 | initialize(app) { 7 | initialize(app, { 8 | store: { 9 | identifier: 'store', 10 | factory: Store 11 | }, 12 | // service: { 13 | // enabled: true, 14 | // name: 'store' 15 | // }, 16 | // development: { 17 | // enabled: true, 18 | // logging: true, 19 | // export: 'store' 20 | // } 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/query/array.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { root, activate } from 'zuglet/decorators'; 3 | import { inject as service } from '@ember/service'; 4 | import { setGlobal, toString } from 'zuglet/utils'; 5 | 6 | @root() 7 | export default class RouteQueryArrayComponent extends Component { 8 | 9 | @service 10 | store 11 | 12 | @activate().content(({ store }) => store.collection('messages').query()) 13 | query 14 | 15 | constructor() { 16 | super(...arguments); 17 | setGlobal({ component: this }); 18 | } 19 | 20 | toString() { 21 | return toString(this); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /firebase/standalone/lib/update-user-email.js: -------------------------------------------------------------------------------- 1 | let { withContext, argv, exit } = require('./setup'); 2 | 3 | let { project, email, uid } = argv; 4 | if(!project) { 5 | exit('--project= is required'); 6 | } 7 | if(!email) { 8 | exit('--email= is required'); 9 | } 10 | if(!uid) { 11 | exit('--uid= is required'); 12 | } 13 | 14 | withContext(project, async ctx => { 15 | 16 | console.log('projectId:', ctx.config.firebase.projectId); 17 | console.log('uid:', uid); 18 | console.log('email:', email); 19 | 20 | let result = await ctx.auth.updateUser(uid, { email }); 21 | console.log(); 22 | console.log(result); 23 | console.log(); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/playground/route.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | import { setGlobal } from 'zuglet/utils'; 5 | 6 | @route() 7 | export default class RouteRoute extends Route { 8 | 9 | @service 10 | store 11 | 12 | // is activated when this returns 13 | async model() { 14 | return this.store.models.create('messages'); 15 | } 16 | 17 | // right after model is activated 18 | // optionally preload data before model() hook resolves 19 | async load(model) { 20 | setGlobal({ model }); 21 | await model.load(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/messages.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 14 |
15 | {{yield}} 16 |
17 | 18 |
-------------------------------------------------------------------------------- /tests/dummy/app/routes/playground/messages/message/index.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | 5 | @route() 6 | export default class MessagesMessageIndexRoute extends Route { 7 | 8 | @service 9 | store 10 | 11 | model() { 12 | let message = this.modelFor('playground.messages.message'); 13 | return this.store.models.create('pages/messages/message/index', { message }); 14 | } 15 | 16 | async load(model) { 17 | await model.load(); 18 | // let loaded = await model.load(); 19 | // if(!loaded) { 20 | // this.transitionTo('playground.messages'); 21 | // } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /addon/-private/model/decorators/cached.js: -------------------------------------------------------------------------------- 1 | import { getState } from '../state'; 2 | import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; 3 | 4 | export const cached = () => (_, key, descriptor) => { 5 | return { 6 | get() { 7 | let state = getState(this); 8 | let cache = state.cache[key]; 9 | if(!cache) { 10 | cache = createCache(() => descriptor.get.call(this)) 11 | state.cache[key] = cache; 12 | } 13 | return getValue(cache); 14 | } 15 | }; 16 | } 17 | 18 | export const getCached = (owner, key) => { 19 | let state = getState(owner); 20 | let cache = state.cache[key]; 21 | if(!cache) { 22 | return; 23 | } 24 | return getValue(cache); 25 | } 26 | -------------------------------------------------------------------------------- /firebase/standalone/lib/create-sign-in-link.js: -------------------------------------------------------------------------------- 1 | let { withContext, argv, exit } = require('./setup'); 2 | 3 | let { project, email, url } = argv; 4 | if(!project) { 5 | exit('--project= is required'); 6 | } 7 | if(!email) { 8 | exit('--email= is required'); 9 | } 10 | if(!url) { 11 | exit('--url= is required'); 12 | } 13 | 14 | withContext(project, async ctx => { 15 | 16 | console.log('projectId:', ctx.config.firebase.projectId); 17 | console.log('email:', email); 18 | 19 | let opts = { 20 | handleCodeInApp: true, 21 | url 22 | }; 23 | 24 | let result = await ctx.auth.generateSignInWithEmailLink(email, opts); 25 | console.log(); 26 | console.log(result); 27 | console.log(); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/playground/messages/message.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { inject as service } from '@ember/service'; 3 | import { route } from 'zuglet/decorators'; 4 | 5 | @route() 6 | export default class MessagesMessageRoute extends Route { 7 | 8 | @service store; 9 | @service router; 10 | 11 | model({ message_id: id }) { 12 | let messages = this.modelFor('playground.messages'); 13 | return this.store.models.create('pages/messages/message', { messages, id }); 14 | } 15 | 16 | async load(model) { 17 | await model.load(); 18 | } 19 | 20 | afterModel(model) { 21 | if(!model.message) { 22 | this.router.transitionTo('playground.messages'); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /docs/api/functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions 3 | pos: 6 4 | --- 5 | 6 | # Functions 7 | 8 | ``` javascript 9 | let functions = store.functions; 10 | let response = await functions.call('hello', { ok: true }); 11 | response // → { data: { … } } 12 | ``` 13 | 14 | ## async call(name, props) `→ object` 15 | 16 | Calls Firebase callable function in default region. 17 | 18 | Alias for `functions.region().call(...args)` 19 | 20 | ## region() 21 | 22 | Returns default region. 23 | 24 | See [Store](api/store) `options.functions` on how to setup custom default region. 25 | 26 | ## region(identifier) 27 | 28 | Returns custom region. 29 | 30 | ``` javascript 31 | let region = functions.region('europe-west1'); 32 | await region.call('hello', { ok: true }); 33 | ``` 34 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/query/single.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { root, activate } from 'zuglet/decorators'; 3 | import { inject as service } from '@ember/service'; 4 | import { setGlobal, toString } from 'zuglet/utils'; 5 | 6 | @root() 7 | export default class RouteQuerySingleComponent extends Component { 8 | 9 | @service 10 | store 11 | 12 | @activate() 13 | .content(({ store }) => { 14 | return store.collection('messages').where('name', '==', 'first').limit(1).query({ type: 'single' }); 15 | }) 16 | query 17 | 18 | constructor() { 19 | super(...arguments); 20 | setGlobal({ component: this }); 21 | } 22 | 23 | toString() { 24 | return toString(this); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /addon/-private/store/auth/methods/popup/google.js: -------------------------------------------------------------------------------- 1 | import Method from '../method'; 2 | import firebase from "firebase/compat/app"; 3 | import { registerPromise } from '../../../../stores/stats'; 4 | 5 | export default class PopupGoogleAuthMethod extends Method { 6 | 7 | /* istanbul ignore next */ 8 | signIn(scopes) { 9 | scopes = scopes || [ 'profile', 'email' ]; 10 | return this.auth._withAuthReturningUser(async auth => { 11 | let provider = new firebase.auth.GoogleAuthProvider(); 12 | scopes.forEach(scope => provider.addScope(scope)); 13 | let { user, credential: { accessToken } } = await registerPromise(this, 'sign-in', false, auth.signInWithPopup(provider)); 14 | return { user, google: { accessToken } }; 15 | }); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /firebase/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "14" 14 | }, 15 | "main": "index.js", 16 | "dependencies": { 17 | "firebase-admin": "^10.0.1", 18 | "firebase-functions": "^3.16.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^7.19.0", 22 | "eslint-plugin-promise": "^4.3.1", 23 | "firebase-functions-test": "^0.3.0" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /addon/-private/model/tracking/tag.js: -------------------------------------------------------------------------------- 1 | import { tracked } from '@glimmer/tracking'; 2 | 3 | class Tag { 4 | 5 | @tracked 6 | __tag__ 7 | 8 | consume() { 9 | this.__tag__ 10 | } 11 | 12 | dirty() { 13 | this.__tag__ = undefined; 14 | } 15 | 16 | } 17 | 18 | const TAGS = new WeakMap(); 19 | 20 | const getTag = (object, key) => { 21 | let tags = TAGS.get(object); 22 | if(!tags) { 23 | tags = new Map(); 24 | TAGS.set(object, tags); 25 | } 26 | 27 | let tag = tags.get(key); 28 | if(!tag) { 29 | tag = new Tag(); 30 | tags.set(key, tag); 31 | } 32 | 33 | return tag; 34 | } 35 | 36 | export const consumeKey = (object, key) => { 37 | getTag(object, key).consume(); 38 | } 39 | 40 | export const dirtyKey = (object, key) => { 41 | getTag(object, key).dirty(); 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - 'docs/**' 8 | pull_request: 9 | branches: [ master ] 10 | paths-ignore: 11 | - 'docs/**' 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Turnstyle 19 | uses: softprops/turnstyle@v1 20 | with: 21 | same-branch-only: false 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Setup Node 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: '14.x' 28 | - name: NPM Install 29 | run: npm install # --no-package-lock 30 | - name: Test 31 | run: npm run test 32 | -------------------------------------------------------------------------------- /addon/-private/model/properties/activate/writable.js: -------------------------------------------------------------------------------- 1 | import BaseActivateProperty from './activate'; 2 | import { consumeKey, dirtyKey } from '../../tracking/tag'; 3 | 4 | export default class WritableActivateProperty extends BaseActivateProperty { 5 | 6 | getPropertyValue() { 7 | consumeKey(this, 'activator'); 8 | let { activator } = this; 9 | if(!activator) { 10 | return null; 11 | } 12 | return activator.getValue(); 13 | } 14 | 15 | setPropertyValue(value) { 16 | let { activator } = this; 17 | if(!activator) { 18 | activator = this.createActivator(value); 19 | this.activator = activator; 20 | } else { 21 | this.assertActivatorType(activator, value); 22 | } 23 | dirtyKey(this, 'activator'); 24 | return activator.setValue(value); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /addon/-private/util/listeners.js: -------------------------------------------------------------------------------- 1 | import { A } from '@ember/array'; 2 | 3 | export class Listeners { 4 | 5 | constructor() { 6 | this._types = Object.create(null); 7 | } 8 | 9 | _type(name) { 10 | let type = this._types[name]; 11 | if(!type) { 12 | type = A(); 13 | this._types[name] = type; 14 | } 15 | return type; 16 | } 17 | 18 | register(name, fn) { 19 | let type = this._type(name); 20 | type.pushObject(fn); 21 | let removed = false; 22 | return () => { 23 | if(removed) { 24 | return false; 25 | } 26 | removed = true; 27 | type.removeObject(fn); 28 | return true; 29 | } 30 | } 31 | 32 | notify(name, ...args) { 33 | let type = [ ...this._type(name) ]; 34 | type.forEach(fn => fn(...args)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /addon/components/zuglet/stats.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { setGlobal } from 'zuglet/utils'; 4 | import { getStats } from '../../-private/stores/stats'; 5 | import { getStores } from '../../-private/stores/get-stores'; 6 | 7 | export default class ZugletStatsComponent extends Component { 8 | 9 | get showStores() { 10 | return this.args.stores !== false; 11 | } 12 | 13 | get stores() { 14 | return getStores(this).stores; 15 | } 16 | 17 | get hasSingleStore() { 18 | return this.stores.length === 1; 19 | } 20 | 21 | get firstStore() { 22 | return this.stores[0]; 23 | } 24 | 25 | get stats() { 26 | return getStats(this); 27 | } 28 | 29 | @action 30 | setGlobal(model) { 31 | setGlobal({ model }); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /docs/decorators/route.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: route 3 | pos: 0 4 | --- 5 | 6 | # @route 7 | 8 | `@route` decorator activates route's model while route is active. 9 | 10 | ``` javascript 11 | // routes/index.js 12 | import Route from '@ember/routing/route'; 13 | import { inject as service } from '@ember/service'; 14 | import { route } from 'zuglet/decorators'; 15 | 16 | @route() 17 | export default class IndexRoute extends Route { 18 | 19 | @service 20 | store 21 | 22 | // returned model is activated 23 | async model() { 24 | return this.store.doc('messages/first').existing(); 25 | } 26 | 27 | // right after model is activated 28 | // in this case document has started observing onSnapshot 29 | // doc.promise resolves when 1st onSnapshot is processed 30 | async load(doc) { 31 | await doc.promise; 32 | } 33 | 34 | } 35 | 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/decorators/root.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: root 3 | pos: 1 4 | --- 5 | 6 | # @root 7 | 8 | `@root` activates `@activate`, `@model`, `@models` properties on first access and deactivates when decorated class is destroyed. 9 | 10 | It is most useful in `Component` context. 11 | 12 | ``` javascript 13 | import Component from '@glimmer/component'; 14 | import { inject as service } from '@ember/service'; 15 | import { root, activate } from 'zuglet/decorators'; 16 | 17 | @root() 18 | export default class NiceComponent extends Component { 19 | 20 | @service 21 | store 22 | 23 | @tracked 24 | id 25 | 26 | // doc is created and activated on 1st access from template or component class 27 | // it is deactivated when component is destroyed 28 | @activate().content(({ store, id }) => store.doc(`messages/${id}`).existing()) 29 | doc 30 | 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/breakpoints.scss: -------------------------------------------------------------------------------- 1 | @mixin breakpoint($point) { 2 | @if $point == mobile { 3 | @media only screen and (max-width: 529px) { 4 | @content; 5 | } 6 | } 7 | @else if $point == tablet { 8 | @media only screen and (min-width: 530px) and (max-width: 949px) { 9 | @content; 10 | } 11 | } 12 | @else if $point == desktop { 13 | @media only screen and (min-width: 950px) and (max-width: 1128px) { 14 | @content; 15 | } 16 | } 17 | @else if $point == desktop-xl { 18 | @media only screen and (min-width: 1129px) { 19 | @content; 20 | } 21 | } 22 | 23 | @else if $point == non-desktop { 24 | @media only screen and (max-width: 949px) { 25 | @content; 26 | } 27 | } 28 | @else if $point == post-tablet { 29 | @media only screen and (min-width: 950px) { 30 | @content; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /docs/api/storage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Storage 3 | pos: 4 4 | --- 5 | 6 | # Storage 7 | 8 | `Storage` manages Firebase Storage uploads, urls and metadata. 9 | 10 | ``` javascript 11 | let storage = store.storage; 12 | ``` 13 | 14 | ## bucket 15 | 16 | Name of default storage bucket 17 | 18 | ## ref(path) `→ Reference` 19 | 20 | Alias for `ref({ path })` 21 | 22 | ## ref({ path }) `→ Reference` 23 | 24 | Creates a storage reference for path in default bucket. 25 | 26 | ``` javascript 27 | let ref = store.storage.ref('hello'); 28 | ref.path // → hello 29 | ref.bucket // → project-id.appspot.com 30 | ``` 31 | 32 | ## ref({ url }) `→ Reference` 33 | 34 | Creates a storage reference with `gs://` url. 35 | 36 | ``` javascript 37 | let ref = store.storage.ref({ url: 'gs://foobar.appspot.com/hello' }); 38 | ref.path // → hello 39 | ref.bucket // → foobar.appspot.com 40 | ``` 41 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/document.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { root, activate } from 'zuglet/decorators'; 3 | import { inject as service } from '@ember/service'; 4 | import { setGlobal, toString } from 'zuglet/utils'; 5 | import { action } from '@ember/object'; 6 | import { alias } from 'macro-decorators'; 7 | 8 | @root() 9 | export default class RouteDocumentComponent extends Component { 10 | 11 | @service store; 12 | 13 | @activate() 14 | .content(({ store }) => store.doc('messages/first').existing()) 15 | doc; 16 | 17 | @alias('doc.data.name') name; 18 | 19 | constructor() { 20 | super(...arguments); 21 | setGlobal({ component: this }); 22 | } 23 | 24 | @action 25 | save() { 26 | this.doc.save(); 27 | } 28 | 29 | toString() { 30 | return toString(this); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /addon/-private/object.js: -------------------------------------------------------------------------------- 1 | import { setOwner } from '@ember/application'; 2 | import { assert } from '@ember/debug'; 3 | import { toString } from './util/to-string'; 4 | import { isFunction } from './util/types'; 5 | 6 | export const setProperties = (object, hash, diff=true) => { 7 | for(let key in hash) { 8 | let value = hash[key]; 9 | if(diff && object[key] === value) { 10 | continue; 11 | } 12 | object[key] = value; 13 | } 14 | }; 15 | 16 | export default class ZugletObject { 17 | 18 | constructor(owner) { 19 | assert(`owner must be owner object`, owner && isFunction(owner.lookup)); 20 | setOwner(this, owner); 21 | } 22 | 23 | toString() { 24 | let extension; 25 | if(isFunction(this.toStringExtension)) { 26 | extension = this.toStringExtension(); 27 | } 28 | return toString(this, extension); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /addon/components/zuglet/stalled.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { later, cancel } from '@ember/runloop'; 3 | import { getStores } from '../../utils'; 4 | import { tracked } from '@glimmer/tracking'; 5 | 6 | export default class ZugletStalledComponent extends Component { 7 | 8 | constructor() { 9 | super(...arguments); 10 | this.stores = getStores(this); 11 | this.next(); 12 | } 13 | 14 | @tracked stalledPromises = false; 15 | 16 | update() { 17 | let stalled = this.stores.stats.stalledPromises; 18 | this.stalledPromises = stalled.map(promise => promise.stats); 19 | } 20 | 21 | next() { 22 | this.cancel = later(() => { 23 | this.update(); 24 | this.next(); 25 | }, 1000); 26 | } 27 | 28 | willDestroy() { 29 | cancel(this.cancel); 30 | super.willDestroy(...arguments); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/playground/navigation.hbs: -------------------------------------------------------------------------------- 1 |
2 | ember-cli-zuglet 3 | doc 4 | query.array 5 | query.single 6 | models 7 | content 8 | reordering 9 | route 10 | auth 11 | storage 12 | functions 13 | messages 14 | _dev 15 |
-------------------------------------------------------------------------------- /vendor/zuglet/fastboot.js: -------------------------------------------------------------------------------- 1 | /* global FastBoot */ 2 | 3 | if(typeof FastBoot !== 'undefined') { 4 | self.XMLHttpRequest = FastBoot.require('xmlhttprequest').XMLHttpRequest; 5 | self.fetch = FastBoot.require('node-fetch'); 6 | if(typeof self.atob === 'undefined') { 7 | self.atob = string => FastBoot.require('buffer').Buffer.from(string, 'base64').toString('binary'); 8 | } 9 | 10 | function createFirebaseModule(name) { 11 | return function() { 12 | 'use strict'; 13 | let mod = FastBoot.require(name); 14 | return { default: mod, __esModule: true }; 15 | } 16 | } 17 | 18 | [ 19 | 'firebase/compat/app', 20 | 'firebase/compat/firestore', 21 | 'firebase/compat/auth', 22 | 'firebase/compat/storage', 23 | 'firebase/compat/functions' 24 | ].forEach(name => { 25 | define(name, [], createFirebaseModule(name)); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /addon/-private/store/auth/methods.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../object'; 2 | import { cached } from '../../model/decorators/cached'; 3 | import { getFactory } from '../../factory/get-factory'; 4 | 5 | export default class AuthMethods extends ZugletObject { 6 | 7 | constructor(owner, { auth }) { 8 | super(owner); 9 | this.auth = auth; 10 | } 11 | 12 | _method(name) { 13 | let { auth } = this; 14 | return getFactory(this).zuglet.create(`store/auth/methods/${name}`, { auth }); 15 | } 16 | 17 | @cached() 18 | get anonymous() { 19 | return this._method('anonymous'); 20 | } 21 | 22 | @cached() 23 | get email() { 24 | return this._method('email'); 25 | } 26 | 27 | @cached() 28 | get popup() { 29 | return this._method('popup'); 30 | } 31 | 32 | @cached 33 | get token() { 34 | return this._method('token'); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | 10 | # misc 11 | /.bowerrc 12 | /.editorconfig 13 | /.ember-cli 14 | /.env* 15 | /.eslintcache 16 | /.eslintignore 17 | /.eslintrc.js 18 | /.git/ 19 | /.github/ 20 | /.gitignore 21 | /.prettierignore 22 | /.prettierrc.js 23 | /.template-lintrc.js 24 | /.travis.yml 25 | /.watchmanconfig 26 | /bower.json 27 | /config/ember-try.js 28 | /CONTRIBUTING.md 29 | /ember-cli-build.js 30 | /testem.js 31 | /tests/ 32 | /yarn-error.log 33 | /yarn.lock 34 | .gitkeep 35 | 36 | # ember-try 37 | /.node_modules.ember-try/ 38 | /bower.json.ember-try 39 | /npm-shrinkwrap.json.ember-try 40 | /package.json.ember-try 41 | /package-lock.json.ember-try 42 | /yarn.lock.ember-try 43 | 44 | /coverage/ 45 | /docs/ 46 | /firebase/ 47 | /.github/ 48 | /server/ 49 | /jsconfig.json 50 | /NOTES.md 51 | 52 | /config.js 53 | -------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/files/__root__/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
{{this.model}}
2 |
{{this.model.data.message}}
3 | 4 | {{!-- --}} 5 | 6 | {{!-- 7 | 8 | .zuglet-stats { 9 | font-family: Menlo, Monaco; 10 | font-size: 12px; 11 | > .section { 12 | margin-bottom: 10px; 13 | &:last-child { 14 | margin-bottom: 0; 15 | } 16 | > .label { 17 | font-weight: 600; 18 | } 19 | } 20 | } 21 | 22 | --}} 23 | 24 | {{!-- 25 | 26 | // jsconfig.json 27 | { 28 | "compilerOptions": { 29 | "target": "es6", 30 | "experimentalDecorators":true 31 | }, 32 | "exclude": [ "node_modules", ".git" ] 33 | } 34 | 35 | // .template-lintrc.js 36 | 'use strict'; 37 | module.exports = { 38 | extends: 'octane', 39 | rules: { 40 | 'no-index-component-invocation': false 41 | } 42 | }; 43 | 44 | --}} -------------------------------------------------------------------------------- /tests/dummy/app/store.js: -------------------------------------------------------------------------------- 1 | import Store from 'zuglet/store'; 2 | import { load } from 'zuglet/utils'; 3 | import environment from './config/environment'; 4 | 5 | let { dummy: { firebase } } = environment; 6 | let persistenceEnabled = environment.environment !== 'test'; 7 | 8 | export default class DummyStore extends Store { 9 | 10 | options = { 11 | firebase, 12 | firestore: { 13 | persistenceEnabled, 14 | // experimentalAutoDetectLongPolling: true, 15 | // experimentalForceLongPolling: true 16 | }, 17 | auth: { 18 | user: 'user' 19 | }, 20 | functions: { 21 | region: null 22 | }, 23 | emulators: { 24 | host: 'localhost', 25 | // auth: 9099, 26 | // firestore: 8080, 27 | // functions: 5001 28 | // storage: 9199 29 | } 30 | } 31 | 32 | async load() { 33 | await load(this.auth); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/auth-methods-anonymos-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | 3 | module('auth / methods / anonymous', function(hooks) { 4 | setupStoreTest(hooks); 5 | 6 | hooks.beforeEach(async function() { 7 | this.auth = this.store.auth; 8 | await this.auth.signOut(); 9 | }); 10 | 11 | hooks.afterEach(async function() { 12 | let user = this.auth.user; 13 | if(user) { 14 | await user.delete(); 15 | } 16 | }); 17 | 18 | test('sign in', async function(assert) { 19 | let user = await this.auth.methods.anonymous.signIn(); 20 | assert.ok(user); 21 | assert.ok(user === this.auth.user); 22 | assert.deepEqual(user.serialized, { 23 | email: null, 24 | emailVerified: false, 25 | isAnonymous: true, 26 | uid: user.uid 27 | }); 28 | assert.ok(typeof user.uid === 'string'); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/functions.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | import { setGlobal, toString } from 'zuglet/utils'; 4 | import { action } from '@ember/object'; 5 | import { tracked } from '@glimmer/tracking'; 6 | 7 | export default class RouteFunctionsComponent extends Component { 8 | 9 | @service 10 | store 11 | 12 | @tracked 13 | response 14 | 15 | constructor() { 16 | super(...arguments); 17 | setGlobal({ component: this }); 18 | } 19 | 20 | @action 21 | async invoke() { 22 | this.response = "Loading…"; 23 | try { 24 | let response = await this.store.functions.call('echo', { now: new Date().toJSON() }); 25 | this.response = response; 26 | } catch(err) { 27 | this.response = err; 28 | } 29 | } 30 | 31 | toString() { 32 | return toString(this); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /addon/-private/model/properties/activate/content.js: -------------------------------------------------------------------------------- 1 | import BaseActivateProperty from './activate'; 2 | import { diff } from '../../decorators/diff'; 3 | 4 | export default class ContentActivateProperty extends BaseActivateProperty { 5 | 6 | @diff() 7 | _value() { 8 | let { owner, opts: { value }, key } = this; 9 | return value.call(owner, owner, key); 10 | } 11 | 12 | getPropertyValue() { 13 | let { activator } = this; 14 | if(!activator) { 15 | let { current } = this._value; 16 | this.value = current; 17 | activator = this.createActivator(current); 18 | this.activator = activator; 19 | } else { 20 | let { current } = this._value; 21 | if(current !== this.value) { 22 | this.value = current; 23 | this.assertActivatorType(activator, current); 24 | activator.setValue(current); 25 | } 26 | } 27 | return activator.getValue(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/remark/link-to.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from "@ember/service"; 3 | import { action } from "@ember/object"; 4 | import { reads } from "macro-decorators"; 5 | 6 | export default class BlockRemarkLinkToComponent extends Component { 7 | 8 | @service router; 9 | 10 | @reads('args.model') model; 11 | 12 | get url() { 13 | let { model: { route, model } } = this; 14 | if(model) { 15 | return this.router.urlFor(route, model); 16 | } else { 17 | return this.router.urlFor(route); 18 | } 19 | } 20 | 21 | @action 22 | transitionTo(e) { 23 | if(e.metaKey) { 24 | return; 25 | } 26 | e.preventDefault(); 27 | let { model: { route, model } } = this; 28 | if(model) { 29 | this.router.transitionTo(route, model); 30 | } else { 31 | this.router.transitionTo(route); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/helpers/setup.js: -------------------------------------------------------------------------------- 1 | import { module, test, only, skip } from 'qunit'; 2 | import { setupTest, setupRenderingTest } from 'ember-qunit'; 3 | import { setupStore } from './setup-store'; 4 | import { setupHelpers } from './setup-helpers'; 5 | 6 | export const setupStoreTest = hooks => { 7 | setupTest(hooks); 8 | setupStore(hooks); 9 | setupHelpers(hooks); 10 | } 11 | 12 | export const setupRenderingStoreTest = (hooks, setup=true) => { 13 | setupRenderingTest(hooks); 14 | if(setup) { 15 | setupStore(hooks); 16 | setupHelpers(hooks); 17 | } 18 | } 19 | 20 | const credentials = { 21 | ampatspell: { email: 'ampatspell@gmail.com', password: 'hello-world' }, // should exist 22 | zeeba: { email: 'zeeba@gmail.com', password: 'R3allyS3cUr3Pas$wooooorrrd.kinda' } // should not exist 23 | }; 24 | 25 | test.only = only; 26 | test.skip = skip; 27 | 28 | export { 29 | module, 30 | test, 31 | credentials 32 | } 33 | -------------------------------------------------------------------------------- /addon/utils.js: -------------------------------------------------------------------------------- 1 | import { setGlobal } from './-private/util/set-global'; 2 | import { objectToJSON } from './-private/util/object-to-json'; 3 | import { toString } from './-private/util/to-string'; 4 | import { toPrimitive } from './-private/util/to-primitive'; 5 | import { toJSON } from './-private/util/to-json'; 6 | import { resolve as load } from './-private/util/resolve'; 7 | import { alive } from './-private/util/alive'; 8 | import { delay } from './-private/util/delay'; 9 | import { getStores } from './-private/stores/get-stores'; 10 | import { isZugletError } from './-private/util/error'; 11 | import { activate, isActivated } from './-private/util/activate'; 12 | import { defer } from './-private/util/defer'; 13 | 14 | export { 15 | getStores, 16 | setGlobal, 17 | objectToJSON, 18 | toString, 19 | toPrimitive, 20 | toJSON, 21 | isZugletError, 22 | load, 23 | alive, 24 | delay, 25 | activate, 26 | isActivated, 27 | defer 28 | }; 29 | -------------------------------------------------------------------------------- /tests/dummy/app/components/input/file.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | const formatBytes = (bytes, decimals = 2) => { 6 | if (bytes === 0) { 7 | return '0 Bytes'; 8 | } 9 | let k = 1024; 10 | let dm = decimals < 0 ? 0 : decimals; 11 | let sizes = ['Bytes', 'Kb', 'Mb', 'Gb', 'Tb', 'PB', 'EB', 'ZB', 'YB']; 12 | let i = Math.floor(Math.log(bytes) / Math.log(k)); 13 | let value = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); 14 | let size = sizes[i]; 15 | return `${value}${size}`; 16 | } 17 | 18 | export default class InputFileComponent extends Component { 19 | 20 | @tracked 21 | file 22 | 23 | get size() { 24 | return formatBytes(this.file.size); 25 | } 26 | 27 | @action 28 | onFiles(e) { 29 | let files = e.target.files; 30 | let file = (files && files[0]) || null; 31 | this.file = file; 32 | this.args.onFile(file); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /firebase/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": [ 8 | "npm --prefix \"$RESOURCE_DIR\" run lint" 9 | ] 10 | }, 11 | "hosting": { 12 | "public": "public", 13 | "ignore": [ 14 | "firebase.json", 15 | "**/.*", 16 | "**/node_modules/**" 17 | ], 18 | "rewrites": [ 19 | { 20 | "source": "**", 21 | "destination": "/index.html" 22 | } 23 | ] 24 | }, 25 | "storage": { 26 | "rules": "storage.rules" 27 | }, 28 | "emulators": { 29 | "auth": { 30 | "port": 9099 31 | }, 32 | "functions": { 33 | "port": 5001 34 | }, 35 | "firestore": { 36 | "port": 8080 37 | }, 38 | "hosting": { 39 | "port": 5000 40 | }, 41 | "storage": { 42 | "port": 9199 43 | }, 44 | "ui": { 45 | "enabled": true, 46 | "port": 3001 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /firebase/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "feathers", 5 | "queryScope": "COLLECTION_GROUP", 6 | "fields": [ 7 | { 8 | "fieldPath": "duck", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "name", 13 | "order": "ASCENDING" 14 | } 15 | ] 16 | } 17 | ], 18 | "fieldOverrides": [ 19 | { 20 | "collectionGroup": "feathers", 21 | "fieldPath": "duck", 22 | "indexes": [ 23 | { 24 | "order": "ASCENDING", 25 | "queryScope": "COLLECTION" 26 | }, 27 | { 28 | "order": "DESCENDING", 29 | "queryScope": "COLLECTION" 30 | }, 31 | { 32 | "arrayConfig": "CONTAINS", 33 | "queryScope": "COLLECTION" 34 | }, 35 | { 36 | "order": "ASCENDING", 37 | "queryScope": "COLLECTION_GROUP" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /tests/dummy/app/components/block/changes.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | // 6 | // 7 | // 8 | // 9 | // 10 | export default class BlockChangesComponent extends Component { 11 | 12 | @tracked 13 | changes = []; 14 | 15 | @action 16 | onPropertyChange(key, value) { 17 | this.changes = [ ...this.changes, { key, value } ]; 18 | } 19 | 20 | @action 21 | clear() { 22 | this.changes = []; 23 | } 24 | 25 | get log() { 26 | return this.changes.map(({ key, value })=> { 27 | if(value === undefined) { 28 | value = '[undefined]'; 29 | } else if(value === null) { 30 | value = '[null]'; 31 | } 32 | return `${key}: ${value}`; 33 | }).join('\n'); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /tests/helpers/util.js: -------------------------------------------------------------------------------- 1 | export const saveCollection = async (ref, docs=[]) => { 2 | let { store } = ref; 3 | await store.batch(batch => docs.map(data => { 4 | let { _id, ...rest } = data; 5 | let doc; 6 | if(_id) { 7 | doc = ref.doc(_id); 8 | } else { 9 | doc = ref.doc(); 10 | } 11 | batch.save(doc.new(rest)); 12 | })); 13 | } 14 | 15 | export const replaceCollection = async (ref, docs=[]) => { 16 | let { store } = ref; 17 | let existing = await ref.load(); 18 | await store.batch(batch => existing.map(doc => batch.delete(doc))); 19 | await saveCollection(ref, docs); 20 | } 21 | 22 | export const poll = async (cb) => { 23 | return new Promise((resolve, reject) => { 24 | let timeout = setTimeout(() => { 25 | reject(new Error('Timeout')); 26 | }, 5000); 27 | let next = () => { 28 | if(!cb()) { 29 | setTimeout(() => next(), 100); 30 | } 31 | clearInterval(timeout); 32 | resolve(); 33 | } 34 | next(); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/references/reference.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../../../object'; 2 | import { toJSON } from '../../../util/to-json'; 3 | 4 | export default class Reference extends ZugletObject { 5 | 6 | store = null; 7 | _ref = null; 8 | 9 | constructor(owner, { store, _ref }) { 10 | super(owner); 11 | this.store = store; 12 | this._ref = _ref; 13 | } 14 | 15 | // 16 | 17 | get _path() { 18 | let ref = this; 19 | while(ref) { 20 | let { path } = ref; 21 | if(path) { 22 | return path; 23 | } 24 | ref = ref.parent; 25 | } 26 | return undefined; 27 | } 28 | 29 | get dashboardURL() { 30 | let { store: { dashboardURL }, _path } = this; 31 | return `${dashboardURL}/firestore/data/${_path}`; 32 | } 33 | 34 | openDashboard() { 35 | window.open(this.dashboardURL, '_blank'); 36 | } 37 | 38 | // 39 | 40 | toJSON() { 41 | let { serialized } = this; 42 | return toJSON(this, { serialized }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /firebase/standalone/lib/delete-users.js: -------------------------------------------------------------------------------- 1 | let { withContext, argv, exit } = require('./setup'); 2 | 3 | let project = argv.project; 4 | if(!project) { 5 | exit('--project= is required'); 6 | } 7 | 8 | withContext(project, async ctx => { 9 | 10 | console.log('projectId:', ctx.config.firebase.projectId); 11 | 12 | let withUsers = async cb => { 13 | let pageToken; 14 | do { 15 | console.log('•', pageToken); 16 | let res = await ctx.auth.listUsers(1000, pageToken); 17 | pageToken = res.pageToken; 18 | await cb(res.users); 19 | } while(pageToken); 20 | } 21 | 22 | await withUsers(async users => { 23 | let uids = users.filter(user => { 24 | if(!user.email) { 25 | return true; 26 | } 27 | if(user.email.startsWith('test-')) { 28 | return true; 29 | } 30 | if(user.email.startsWith('ampatspell+')) { 31 | return true; 32 | } 33 | }).map(user => user.uid); 34 | await ctx.auth.deleteUsers(uids); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /addon/-private/model/tracking/utils.js: -------------------------------------------------------------------------------- 1 | export const propToIndex = prop => { 2 | if(typeof prop === 'symbol') { 3 | return null; 4 | } 5 | let idx = Number(prop); 6 | if(isNaN(idx)) { 7 | return null; 8 | } 9 | return idx; 10 | } 11 | 12 | export const ARRAY_GETTERS = new Set([ 13 | Symbol.iterator, 14 | 'concat', 15 | 'entries', 16 | 'every', 17 | 'fill', 18 | 'filter', 19 | 'find', 20 | 'findIndex', 21 | 'flat', 22 | 'flatMap', 23 | 'forEach', 24 | 'includes', 25 | 'indexOf', 26 | 'join', 27 | 'keys', 28 | 'lastIndexOf', 29 | 'map', 30 | 'reduce', 31 | 'reduceRight', 32 | 'slice', 33 | 'some', 34 | 'values', 35 | ]); 36 | 37 | export const ARRAY_MUTATORS = new Set([ 38 | 'replace', // replace(start, deleteCount, items); 39 | 'copyWithin', 40 | 'pop', // pop() 41 | 'push', // push(...items) 42 | 'reverse', 43 | 'shift', // shift() 44 | 'sort', 45 | 'splice', // splice(start, deleteCount, ...items); 46 | 'unshift', // unshift(...items) 47 | ]); 48 | -------------------------------------------------------------------------------- /docs/api/auth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Auth 3 | pos: 5 4 | --- 5 | 6 | # Auth 7 | 8 | ``` javascript 9 | let auth = store.auth; 10 | ``` 11 | 12 | Auth is activated on first access from store. 13 | 14 | ## user `→ User or null` 15 | 16 | Currently signed in user. 17 | 18 | See [Store](api/store) `options.auth` on how to setup custom user subclass. 19 | 20 | ## promise `→ Promise` 21 | 22 | Resolves on first `onAuthStateChanged` invocation. 23 | 24 | ## async signOut() `→ undefined` 25 | 26 | Signs out current user if there is current user. 27 | 28 | ## methods `→ AuthMethods` 29 | 30 | Auth [methods](api/auth/methods). 31 | 32 | ``` javascript 33 | let auth = store.auth; 34 | await auth.methods.anonymous.signIn(); 35 | ``` 36 | 37 | ## async verifyPasswordResetCode(code) `→ string` 38 | 39 | ``` javascript 40 | let email = await auth.store.verifyPasswordResetCode(code); 41 | ``` 42 | 43 | ## async confirmPasswordReset(code, password) `→ Void` 44 | 45 | ``` javascript 46 | await auth.store.confirmPasswordReset(code, password); 47 | ``` 48 | -------------------------------------------------------------------------------- /addon/-private/model/properties/property/property.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../../object'; 2 | import { getState } from '../../state'; 3 | 4 | export default class Property extends ZugletObject { 5 | 6 | state = null; 7 | owner = null; 8 | key = null; 9 | opts = null; 10 | 11 | constructor(_owner, { state, owner, key, opts }) { 12 | super(_owner); 13 | this.state = state; 14 | this.owner = owner; 15 | this.key = key; 16 | this.opts = opts; 17 | } 18 | 19 | get isActivated() { 20 | return this.state.isActivated; 21 | } 22 | 23 | // 24 | 25 | activateValue(value) { 26 | if(value) { 27 | let state = getState(value); 28 | if(state) { 29 | state.activate(this); 30 | } 31 | } 32 | } 33 | 34 | deactivateValue(value) { 35 | if(value) { 36 | let state = getState(value); 37 | if(state) { 38 | state.deactivate(this); 39 | } 40 | } 41 | } 42 | 43 | // 44 | 45 | onActivated() { 46 | } 47 | 48 | onDeactivated() { 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /addon/-private/store/functions/region.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../../object'; 2 | import { toJSON } from '../../util/to-json'; 3 | import { registerPromise } from '../../stores/stats'; 4 | 5 | export default class FunctionsRegion extends ZugletObject { 6 | 7 | constructor(owner, { functions, _region }) { 8 | super(owner); 9 | this.functions = functions; 10 | this._region = _region; 11 | } 12 | 13 | get identifier() { 14 | return this._region._region; 15 | } 16 | 17 | // 18 | 19 | async call(name, props) { 20 | let callable = this._region.httpsCallable(name); 21 | let result = await registerPromise(this, 'call', false, callable(props)); 22 | return result; 23 | } 24 | 25 | // 26 | 27 | get serialized() { 28 | let { identifier } = this; 29 | return { 30 | identifier 31 | }; 32 | } 33 | 34 | toJSON() { 35 | let { serialized } = this; 36 | return toJSON(this, { serialized }); 37 | } 38 | 39 | toStringExtension() { 40 | return `${this.identifier}`; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/content.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | import { setGlobal, toString } from 'zuglet/utils'; 4 | import { root, activate, models } from 'zuglet/decorators'; 5 | import { tracked } from '@glimmer/tracking'; 6 | 7 | @root() 8 | export default class RouteContentComponent extends Component { 9 | 10 | @service 11 | store 12 | 13 | @tracked 14 | name = 'first' 15 | 16 | @activate() 17 | .content(({ store, name }) => { 18 | let ref = store.collection('messages'); 19 | if(name) { 20 | ref = ref.where('name', '==', name); 21 | } 22 | return ref.query(); 23 | }) 24 | query 25 | 26 | @models() 27 | .source(({ query: { content } }) => content) 28 | .named('message') 29 | .mapping(doc => ({ doc })) 30 | models 31 | 32 | constructor() { 33 | super(...arguments); 34 | setGlobal({ component: this }); 35 | } 36 | 37 | toString() { 38 | return toString(this); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/unit/setup-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | import { isActivated } from 'zuglet/utils'; 3 | import ZugletObject from 'zuglet/object'; 4 | 5 | module('setup', function(hooks) { 6 | setupStoreTest(hooks); 7 | 8 | test('stores exist', function(assert) { 9 | assert.ok(this.stores); 10 | }); 11 | 12 | test('store exists', function(assert) { 13 | assert.ok(this.store); 14 | }); 15 | 16 | test('store is activated', async function(assert) { 17 | assert.ok(isActivated(this.store)); 18 | }); 19 | 20 | test('register model', function(assert) { 21 | class Box extends ZugletObject { 22 | name = 'box' 23 | constructor(owner, { ok }) { 24 | super(owner); 25 | this.ok = ok; 26 | } 27 | } 28 | 29 | this.registerModel('box', Box); 30 | let model = this.store.models.create('box', { ok: true }); 31 | 32 | assert.ok(model); 33 | assert.ok(model instanceof Box); 34 | assert.strictEqual(model.name, 'box'); 35 | assert.strictEqual(model.ok, true); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /tests/dummy/app/services/docs.js: -------------------------------------------------------------------------------- 1 | import FilesService from 'remark/services/files'; 2 | import { cached } from "tracked-toolbox"; 3 | import { inject as service } from "@ember/service"; 4 | import { sortedBy } from '../util/array'; 5 | 6 | const normalize = name => { 7 | if(name && name.endsWith('/')) { 8 | name = name.substring(0, name.length - 1); 9 | } 10 | return name; 11 | } 12 | 13 | export default class DocsService extends FilesService { 14 | 15 | identifier = 'docs'; 16 | 17 | @service store; 18 | 19 | @cached 20 | get pages() { 21 | return sortedBy(this.all.map(file => { 22 | return this.store.models.create('docs/page', { file, docs: this }); 23 | }), page => page.pos); 24 | } 25 | 26 | @cached 27 | get root() { 28 | return this.pages.filter(page => page.directory === ''); 29 | } 30 | 31 | page(name) { 32 | name = normalize(name); 33 | return this.pages.find(page => page.id === name); 34 | } 35 | 36 | directory(name) { 37 | name = normalize(name); 38 | return this.pages.filter(page => page.directory === name); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/transaction.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../../object'; 2 | import { isDocument } from './document'; 3 | import { isDocumentReference } from './references/document'; 4 | import { assert } from '@ember/debug'; 5 | 6 | export default class Transaction extends ZugletObject { 7 | 8 | store = null; 9 | _tx = null; 10 | 11 | constructor(owner, { store, _tx }) { 12 | super(owner); 13 | this.store = store; 14 | this._tx = _tx; 15 | } 16 | 17 | load(arg, opts) { 18 | assert(`argument must be Document or DocumentReference not '${arg}'`, isDocument(arg) || isDocumentReference(arg)); 19 | return arg._loadInternal(ref => this._tx.get(ref), opts); 20 | } 21 | 22 | save(arg, opts) { 23 | assert(`argument must be Document not '${arg}'`, isDocument(arg)); 24 | return arg._saveInternal((...args) => this._tx.set(...args), opts); 25 | } 26 | 27 | delete(arg) { 28 | assert(`argument must be Document not '${arg}'`, isDocument(arg) || isDocumentReference(arg)); 29 | return arg._deleteInternal(ref => this._tx.delete(ref)); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/dummy/app/models/message.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from 'zuglet/object'; 2 | import { objectToJSON, toJSON } from 'zuglet/utils'; 3 | import { tracked } from '@glimmer/tracking'; 4 | import { alias } from 'macro-decorators'; 5 | 6 | export default class Message extends ZugletObject { 7 | 8 | @tracked 9 | doc 10 | 11 | constructor(owner, { doc }) { 12 | super(owner); 13 | this.doc = doc; 14 | } 15 | 16 | get id() { 17 | return this.doc.id; 18 | } 19 | 20 | async save() { 21 | await this.doc.save(); 22 | } 23 | 24 | async delete() { 25 | await this.doc.delete(); 26 | } 27 | 28 | // 29 | 30 | @alias('doc.data.name') 31 | name 32 | 33 | @alias('doc.data.text') 34 | text 35 | 36 | // 37 | 38 | async load() { 39 | } 40 | 41 | // 42 | 43 | get serialized() { 44 | let { doc } = this; 45 | return { 46 | doc: objectToJSON(doc) 47 | }; 48 | } 49 | 50 | toJSON() { 51 | let { serialized } = this; 52 | return toJSON(this, { serialized }); 53 | } 54 | 55 | toStringExtension() { 56 | return `${this.id}`; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/dummy/app/util/array.js: -------------------------------------------------------------------------------- 1 | export const sortedBy = (array, arg) => { 2 | let fn = model => model[arg]; 3 | if(typeof arg === 'function') { 4 | fn = arg; 5 | } 6 | return [ ...array ].sort((a, b) => { 7 | a = fn(a); 8 | b = fn(b); 9 | return a < b ? -1 : a > b ? 1 : 0; 10 | }); 11 | } 12 | 13 | export const firstObject = array => { 14 | return array && array[0]; 15 | } 16 | 17 | export const lastObject = array => { 18 | return array && array[array.length - 1]; 19 | } 20 | 21 | export const nextObject = (array, object, wrap=false) => { 22 | let idx = array.indexOf(object); 23 | if(idx === -1) { 24 | return; 25 | } 26 | if(idx === array.length - 1) { 27 | if(wrap) { 28 | return firstObject(array); 29 | } 30 | return; 31 | } 32 | return array[idx + 1]; 33 | } 34 | 35 | export const prevObject = (array, object, wrap=false) => { 36 | let idx = array.indexOf(object); 37 | if(idx === -1) { 38 | return; 39 | } 40 | if(idx === 0) { 41 | if(wrap) { 42 | return lastObject(array); 43 | } 44 | return; 45 | } 46 | return array[idx - 1]; 47 | } 48 | -------------------------------------------------------------------------------- /docs/api/firestore/batch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Batch 3 | pos: 3 4 | --- 5 | 6 | # Batch 7 | 8 | Creates and commits a Firestore document batch. 9 | 10 | ``` javascript 11 | await store.batch(batch => { 12 | batch.save(store.collection('messages').doc().new({ title: 'first' })); 13 | batch.save(store.collection('messages').doc().new({ title: 'second' })); 14 | }); 15 | ``` 16 | 17 | ## async commit() 18 | 19 | Commits a batch for `store.batch()` use. 20 | 21 | ``` javascript 22 | let batch = store.batch(); 23 | batch.save(store.collection('messages').doc().new({ title: 'first' })); 24 | batch.save(store.collection('messages').doc().new({ title: 'second' })); 25 | await batch.commit(); 26 | ``` 27 | 28 | ## save(doc, opts) 29 | 30 | Saves a document in batch. See [Document](api/firestore/document) `save()` for options. 31 | 32 | ## delete(arg) 33 | 34 | * arg: `Document` or `DocumentReference` 35 | 36 | Deletes a document in batch. 37 | 38 | ``` javascript 39 | let ref = store.doc('messages/first'); 40 | let doc = ref.existing(); 41 | 42 | await store.batch(batch => { 43 | batch.delete(ref); 44 | batch.delete(doc); 45 | }); 46 | ``` 47 | -------------------------------------------------------------------------------- /tests/unit/models-classic-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | import { getOwner } from '@ember/application'; 3 | import { inject as service } from '@ember/service'; 4 | import { guidFor } from '@ember/object/internals'; 5 | import EmberObject from '@ember/object'; 6 | 7 | module('models / classic', function(hooks) { 8 | setupStoreTest(hooks); 9 | 10 | test('create classic model', async function(assert) { 11 | class Hamster extends EmberObject { 12 | 13 | @service store 14 | 15 | toStringExtension() { 16 | let { id, name } = this; 17 | return `${id}:${name}`; 18 | } 19 | 20 | } 21 | 22 | this.registerModel('hamster', Hamster); 23 | 24 | let model = this.store.models.create('hamster', { id: 'z33ba', name: 'Zeeba' }); 25 | assert.ok(model); 26 | assert.strictEqual(model.id, 'z33ba'); 27 | assert.strictEqual(model.name, 'Zeeba'); 28 | assert.strictEqual(model.store, getOwner(this.store).lookup('service:store')); 29 | assert.strictEqual(String(model), ``); 30 | }); 31 | 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /tests/unit/storage-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | 3 | module('storage', function(hooks) { 4 | setupStoreTest(hooks); 5 | 6 | hooks.beforeEach(function(assert) { 7 | this.bucket = this.store.options.firebase.storageBucket; 8 | assert.ok(this.bucket); 9 | }); 10 | 11 | test('bucket', async function(assert) { 12 | let bucket = this.store.storage.bucket; 13 | assert.strictEqual(bucket, this.bucket); 14 | }); 15 | 16 | test('serialized', function(assert) { 17 | assert.deepEqual(this.store.storage.serialized, { 18 | bucket: this.bucket 19 | }); 20 | }); 21 | 22 | test('toJSON', function(assert) { 23 | let json = this.store.storage.toJSON(); 24 | assert.deepEqual(json, { 25 | instance: json.instance, 26 | serialized: { 27 | bucket: this.bucket 28 | } 29 | }); 30 | assert.ok(json.instance.startsWith('zuglet@store/storage::ember')); 31 | }); 32 | 33 | test('toStringExtension', function(assert) { 34 | let string = this.store.storage.toStringExtension(); 35 | assert.strictEqual(string, this.bucket); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /addon/-private/model/state/activators.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | import { removeObject } from '../../util/array'; 3 | 4 | class Activator { 5 | 6 | constructor(object) { 7 | this.object = object; 8 | this.count = 1; 9 | } 10 | 11 | inc() { 12 | this.count++; 13 | } 14 | 15 | dec() { 16 | this.count--; 17 | return this.count === 0; 18 | } 19 | 20 | } 21 | 22 | export default class Activators { 23 | 24 | activators = []; 25 | 26 | _find(object) { 27 | return this.activators.find(activator => activator.object === object); 28 | } 29 | 30 | add(object) { 31 | let activator = this._find(object); 32 | if(activator) { 33 | activator.inc(); 34 | } else { 35 | activator = new Activator(object); 36 | this.activators.push(activator); 37 | } 38 | } 39 | 40 | delete(object) { 41 | let activator = this._find(object); 42 | assert(`Activator not found for object ${object}`, !!activator); 43 | if(activator.dec()) { 44 | removeObject(this.activators, activator); 45 | } 46 | } 47 | 48 | get size() { 49 | return this.activators.length; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /addon/-private/model/properties/activate.js: -------------------------------------------------------------------------------- 1 | import { property } from './property'; 2 | import { isFunction } from '../../util/types'; 3 | import { assert } from '@ember/debug'; 4 | 5 | let getProperty = (owner, key, opts) => property(owner, key, `activate/${opts.type}`, opts); 6 | 7 | const define = opts => (_, key) => { 8 | let get = function() { 9 | return getProperty(this, key, opts).getPropertyValue(); 10 | } 11 | if(opts.type === 'content') { 12 | return { 13 | get 14 | }; 15 | } else { 16 | let set = function(value) { 17 | return getProperty(this, key, opts).setPropertyValue(value); 18 | } 19 | return { 20 | get, 21 | set 22 | }; 23 | } 24 | } 25 | 26 | export const activate = () => { 27 | 28 | let opts = { 29 | type: 'writable' 30 | }; 31 | 32 | let extend = () => { 33 | let curr = define(opts); 34 | curr.content = value => { 35 | assert(`@activate().content(fn) must be function not '${value}'`, isFunction(value)); 36 | opts.value = value; 37 | opts.type = 'content'; 38 | return extend(); 39 | } 40 | return curr; 41 | } 42 | 43 | return extend(); 44 | } 45 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/query/single.js: -------------------------------------------------------------------------------- 1 | import Query from './query'; 2 | 3 | export default class QuerySingle extends Query { 4 | 5 | _first(_snapshot) { 6 | let snapshot = _snapshot.docs[0]; 7 | if(_snapshot.docs.length > 1) { 8 | console.warn(`${this.ref.string}.query({ type: 'single' }) yields more than 1 document`); 9 | } 10 | return snapshot; 11 | } 12 | 13 | _onSnapshotInternal(_snapshot) { 14 | let snapshot = this._first(_snapshot); 15 | let { content } = this; 16 | if(snapshot) { 17 | if(content && content.path === snapshot.ref.path) { 18 | content._onSnapshot(snapshot, { source: 'subscription' }); 19 | content._onSnapshotMetadata(snapshot); 20 | } else { 21 | this.content = this._createDocumentForSnapshot(snapshot); 22 | } 23 | } else { 24 | if(content) { 25 | content._onDeleted(); 26 | } 27 | this.content = null; 28 | } 29 | this._notifyOnData(); 30 | } 31 | 32 | _onSnapshot(snapshot) { 33 | this._onSnapshotInternal(snapshot); 34 | } 35 | 36 | _onLoad(snapshot) { 37 | this._onSnapshotInternal(snapshot); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /firebase/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | service cloud.firestore { 4 | match /databases/{database}/documents { 5 | 6 | function isEqualToString(data, key, expected) { 7 | return key in data.keys() && data.get(key, null) == expected; 8 | } 9 | 10 | match /messages/{message} { 11 | allow read: if true; 12 | allow create: if !isEqualToString(request.resource.data, 'name', 'fail'); 13 | allow update: if !isEqualToString(request.resource.data, 'name', 'fail'); 14 | allow delete: if true; 15 | } 16 | 17 | match /ducks/{duck} { 18 | allow read: if true; 19 | allow create: if !isEqualToString(request.resource.data, 'name', 'fail'); 20 | allow update: if !isEqualToString(request.resource.data, 'name', 'fail'); 21 | allow delete: if true; 22 | } 23 | 24 | match /{path=**}/feathers/{feather} { 25 | allow read: if true; 26 | allow write: if true; 27 | } 28 | 29 | match /posts/{post} { 30 | allow read: if true; 31 | allow write: if true; 32 | } 33 | 34 | match /{path=**}/messages/{doc} { 35 | allow read: if true; 36 | } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/api/object.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Object 3 | pos: 8 4 | --- 5 | 6 | # Object 7 | 8 | Convinient base class for models which 9 | 10 | * sets Ember.js owner to instance so that services can be injected 11 | * overrides `toString()` and registers model name which is not stripped away in production builds 12 | 13 | ``` javascript 14 | export default class ZugletObject { 15 | 16 | constructor(owner) { 17 | setOwner(this, owner); 18 | } 19 | 20 | toString() { 21 | // … 22 | } 23 | 24 | } 25 | ``` 26 | 27 | Usage 28 | 29 | ``` javascript 30 | // app/models/duck.js 31 | import ZugletObject from 'zuglet/object'; 32 | import { inject as service } from '@ember/service'; 33 | 34 | export default class Duck extends ZugletObject { 35 | 36 | @service 37 | store 38 | 39 | constructor(owner, { name }) { 40 | super(owner); 41 | this.name = name; 42 | } 43 | 44 | toStringExtension() { 45 | return this.name; 46 | } 47 | 48 | } 49 | ``` 50 | 51 | ``` javascript 52 | let duck = store.models.create('duck', { name: 'Yellow' }); 53 | String(duck) // → 54 | duck.store // → 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/api/firestore/transaction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Transaction 3 | pos: 4 4 | --- 5 | 6 | # Transaction 7 | 8 | Performs multiple document operations in transaction. 9 | 10 | ``` javascript 11 | let result = await store.transaction(async tx => { 12 | 13 | let doc = await tx.load(store.doc('messages/first')); 14 | doc.data.count++; 15 | await tx.save(doc); 16 | 17 | return { ok: true }; 18 | }); 19 | 20 | result // → { ok: true } 21 | ``` 22 | 23 | ## async load(arg, opts) `→ Document or undefined` 24 | 25 | Loads a document in transaction. 26 | 27 | * `arg` → `Document` or `DocumentReference` 28 | * `opts` → load options. 29 | 30 | See [Document](api/firestore/document) and [DocumentReference](api/firestore/reference/document) `load()` for options 31 | 32 | ## async save(doc, opts) `→ Document` 33 | 34 | Saves Document in transaction. 35 | 36 | ## async delete(arg) 37 | 38 | Deletes document in transaction. 39 | 40 | * `arg` → `Document` or `DocumentReference` 41 | 42 | ``` javascript 43 | let ref = store.doc('messages/first'); 44 | let doc = ref.existing(); 45 | 46 | await store.transaction(async tx => { 47 | await tx.delete(ref); 48 | await tx.delete(doc); 49 | }); 50 | ``` 51 | -------------------------------------------------------------------------------- /tests/unit/functions-region-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | 3 | module('functions / region', function(hooks) { 4 | setupStoreTest(hooks); 5 | 6 | test('call', async function(assert) { 7 | let result = await this.store.functions.region('us-central1').call('echo', { ok: true }); 8 | assert.deepEqual(result, { 9 | data: { 10 | data: { 11 | ok: true 12 | }, 13 | uid: null 14 | } 15 | }); 16 | }); 17 | 18 | test('serialized', function(assert) { 19 | assert.deepEqual(this.store.functions.region().serialized, { 20 | identifier: 'us-central1' 21 | }); 22 | }); 23 | 24 | test('toJSON', function(assert) { 25 | let json = this.store.functions.region().toJSON(); 26 | assert.deepEqual(json, { 27 | instance: json.instance, 28 | serialized: { 29 | identifier: 'us-central1' 30 | } 31 | }); 32 | assert.ok(json.instance.startsWith('zuglet@store/functions/region::ember')); 33 | }); 34 | 35 | test('toStringExtension', function(assert) { 36 | assert.strictEqual(this.store.functions.region().toStringExtension(), 'us-central1'); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /addon/-private/model/properties/activate/activators/object.js: -------------------------------------------------------------------------------- 1 | 2 | export default class ObjectActivator { 3 | 4 | type = 'object'; 5 | 6 | value = null; 7 | isActivated = false; 8 | 9 | constructor(property, value) { 10 | this.property = property; 11 | this.isActivated = false; 12 | this.value = value || null; 13 | this.activate(); 14 | } 15 | 16 | activate() { 17 | if(!this.property.isActivated) { 18 | return; 19 | } 20 | 21 | if(this.isActivated) { 22 | return; 23 | } 24 | 25 | this.isActivated = true; 26 | 27 | let value = this.value; 28 | this.property.activateValue(value); 29 | } 30 | 31 | deactivate() { 32 | if(!this.isActivated) { 33 | return; 34 | } 35 | 36 | this.isActivated = false; 37 | 38 | let value = this.value; 39 | this.property.deactivateValue(value); 40 | } 41 | 42 | getValue() { 43 | this.activate(); 44 | return this.value; 45 | } 46 | 47 | setValue(value) { 48 | if(value === this.value) { 49 | return; 50 | } 51 | value = value || null; 52 | this.deactivate(); 53 | this.value = value; 54 | this.activate(); 55 | return value; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /docs/api/stores.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Stores 3 | pos: 7 4 | --- 5 | 6 | # Stores 7 | 8 | Stores manages any [Store](api/store) instances used in the app. 9 | 10 | Singleton, accessible using `getStores()` helper 11 | 12 | ``` javascript 13 | import { getStores } from 'zuglet/utils'; 14 | let stores = getStores(this); 15 | ``` 16 | 17 | ## stats `→ Stats` 18 | 19 | Returns singleton [Stats](api/stores/stats) instance. 20 | 21 | ## models `→ Models` 22 | 23 | Returns singleton [Models](api/models) instance. 24 | 25 | ## createStore(identifier, factory) `→ Store` 26 | 27 | Creates a new store. 28 | 29 | ``` javascript 30 | import BaseStore from 'zuglet/store'; 31 | 32 | class Store extends BaseStore { 33 | } 34 | 35 | let store = getStores(this).createStore('store', Store); 36 | ``` 37 | 38 | ## store(identifier, { optional }) 39 | 40 | Returns existing store. 41 | 42 | * `optional` → boolean, defaults to false 43 | 44 | ``` javascript 45 | let missing = getStores(this).store('missing', { optional: true }); 46 | missing // → undefined 47 | 48 | let existing = getStores(this).store('existing'); 49 | existing // → Store 50 | ``` 51 | 52 | ## async settle() → undefined 53 | 54 | Alias for `stores.stats.settle()` 55 | -------------------------------------------------------------------------------- /docs/api/auth/methods/email.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Email 3 | pos: 1 4 | --- 5 | 6 | # Email 7 | 8 | ## async signIn(email, password) `→ User` 9 | 10 | Sign-in using email and password 11 | 12 | ``` javascript 13 | let user = await store.auth.methods.email.signIn(email, password); 14 | store.auth.user === user // → true 15 | ``` 16 | 17 | ## async signUp(email, password) `→ User` 18 | 19 | Sign up with user and password 20 | 21 | ``` javascript 22 | let user = await store.auth.methods.email.signUp(email, password); 23 | ``` 24 | 25 | ## async sendSignInLink(email, opts) `→ undefined` 26 | 27 | Sends sign in link to provided email address 28 | 29 | ``` javascript 30 | await store.auth.methods.email.sendSignInLink(email, { 31 | url: window.location.href 32 | }); 33 | ``` 34 | 35 | ## async signInWithLink(email, link) `→ User` 36 | 37 | Signs in with a link 38 | 39 | * `email` → user's email address 40 | * `link` → optional, defaults to `window.location.href` 41 | 42 | ``` javascript 43 | let user = await store.auth.methods.email.signInWithLink(email); 44 | ``` 45 | 46 | ## async sendPasswordReset(email, opts) `→ undefined` 47 | 48 | ``` javascript 49 | await store.auth.methods.email.sendPasswordReset(email); 50 | ``` 51 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/reordering.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 | {{#let this.first as |model|}} 15 | #{{model.position}}: {{model.title}} 16 | {{/let}} 17 |
18 | 19 |
20 | {{#each this.query.content as |doc|}} 21 |
22 | #{{doc.data.position}}: {{doc.data.title}} 23 | [Up] 24 | [Down] 25 | [Delete] 26 |
27 | {{/each}} 28 |
29 | 30 |
31 | {{#each this.models as |model|}} 32 |
33 | #{{model.position}}: {{model.title}} 34 |
35 | {{/each}} 36 |
37 | 38 |
-------------------------------------------------------------------------------- /addon/-private/util/resolve.js: -------------------------------------------------------------------------------- 1 | import { isPromise } from './types'; 2 | 3 | const promiseForType = (model, type) => { 4 | if(!model) { 5 | return; 6 | } 7 | let { promise } = model; 8 | if(!promise) { 9 | return; 10 | } 11 | if(type) { 12 | let nested = promise[type]; 13 | if(nested) { 14 | return nested; 15 | } 16 | } 17 | return promise; 18 | } 19 | 20 | const toPromises = (args, type) => { 21 | let models = []; 22 | 23 | args.forEach(arg => { 24 | if(Array.isArray(arg)) { 25 | models.push(...arg); 26 | } else { 27 | models.push(arg); 28 | } 29 | }); 30 | 31 | let promises = []; 32 | 33 | models.forEach(model => { 34 | if(!model) { 35 | return; 36 | } 37 | let promise = promiseForType(model, type); 38 | if(isPromise(promise)) { 39 | promises.push(promise); 40 | } else if(isPromise(model)) { 41 | promises.push(model); 42 | } 43 | }); 44 | 45 | return promises; 46 | }; 47 | 48 | const _resolve = type => (...args) => Promise.all(toPromises(args, type)); 49 | 50 | const resolve = _resolve(); 51 | resolve.cached = _resolve('cached'); 52 | resolve.remote = _resolve('remote'); 53 | 54 | export { 55 | resolve 56 | } 57 | -------------------------------------------------------------------------------- /docs/api/firestore/reference/collection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collection 3 | pos: 3 4 | --- 5 | 6 | # Collection Reference `extends QueryableReference` 7 | 8 | See [Queryable Reference](api/firestore/reference/queryable) for query `load`, `query` and condition methods which are shared between `Collection`, `CollectionGroup` and `Condition` references. 9 | 10 | ``` javascript 11 | let ref = store.collection('messages'); 12 | ``` 13 | 14 | ## id `→ string` 15 | 16 | Document id 17 | 18 | ## path `→ string` 19 | 20 | Document path 21 | 22 | ## parent `→ DocumentReference` 23 | 24 | Creates a new [Document Reference](api/firestore/reference/document) which points to collections's parent 25 | 26 | ``` javascript 27 | let coll = store.doc('users/zeeba/messages'); 28 | let ref = coll.parent; 29 | ref.path // → 'users/zeeba' 30 | ``` 31 | 32 | ## doc(path) 33 | 34 | Creates a new [Document Reference](api/firestore/reference/document) which points to nested document. 35 | 36 | ``` javascript 37 | let coll = store.collection('users'); 38 | let ref = coll.doc('zeeba'); 39 | ref.path // → 'users/zeeba' 40 | ``` 41 | 42 | ## dashboardURL `→ String` 43 | 44 | Firestore dashboard URL. 45 | 46 | ## openDashboard() 47 | 48 | `window.open` Firestore dashboard URL. 49 | -------------------------------------------------------------------------------- /addon/-private/model/state/index.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '../../util/get-owner'; 2 | import { assert } from '@ember/debug'; 3 | 4 | const { 5 | assign 6 | } = Object; 7 | 8 | const marker = Symbol('ZUGLET'); 9 | 10 | const createState = (owner, opts) => { 11 | let _owner = getOwner(owner, opts); 12 | if(!_owner) { 13 | return; 14 | } 15 | let factory; 16 | if(isRoot(owner)) { 17 | factory = _owner.factoryFor('zuglet:state/root'); 18 | } else { 19 | factory = _owner.factoryFor('zuglet:state/model'); 20 | } 21 | 22 | return new factory.class(_owner, { owner }); 23 | } 24 | 25 | export const getState = (owner, opts) => { 26 | let { optional, create } = assign({ optional: false, create: true }, opts); 27 | assert(`owner is required`, !!owner); 28 | 29 | let state = owner[marker]; 30 | if(!state && create) { 31 | state = createState(owner, { optional }); 32 | if(state) { 33 | owner[marker] = state; 34 | state.didCreateState(); 35 | } 36 | } 37 | 38 | return state; 39 | } 40 | 41 | const root = 'root'; 42 | 43 | export const isRoot = instance => { 44 | return instance && instance.constructor[marker] === root; 45 | } 46 | 47 | export const setRoot = Class => { 48 | Class[marker] = root; 49 | } 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MergeTrees = require('broccoli-merge-trees'); 4 | const writeFile = require('broccoli-file-creator'); 5 | 6 | const defaults = { 7 | proxyClassicSupport: false, 8 | stallThreshold: 5000, 9 | version: { 10 | zuglet: require('./package.json').version, 11 | firebase: require('firebase/package.json').version 12 | } 13 | }; 14 | 15 | module.exports = { 16 | name: 'zuglet', 17 | isDevelopingAddon() { 18 | return true; 19 | }, 20 | included(app, parentAddon) { 21 | this._super.included.apply(this, arguments); 22 | this.zuglet = Object.assign({}, defaults, (parentAddon || app).options['zuglet']); 23 | app.import('vendor/zuglet/fastboot.js'); 24 | }, 25 | treeForAddon() { 26 | let tree = this._super.treeForAddon.apply(this, arguments); 27 | return MergeTrees([ 28 | tree, 29 | writeFile('-private/flags.js', `define('zuglet/-private/flags', [ 'exports' ], function(_exports) { 30 | "use strict"; 31 | 32 | Object.defineProperty(_exports, "__esModule", { 33 | value: true 34 | }); 35 | 36 | var _default = Object.freeze(${JSON.stringify(this.zuglet, null, 2)}); 37 | _exports.default = _default; 38 | });`) 39 | ]); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/references/collection.js: -------------------------------------------------------------------------------- 1 | import QueryableReference from './queryable'; 2 | import { cached } from '../../../model/decorators/cached'; 3 | 4 | export default class CollectionReference extends QueryableReference { 5 | 6 | get id() { 7 | return this._ref.id; 8 | } 9 | 10 | get path() { 11 | return this._ref.path; 12 | } 13 | 14 | @cached() 15 | get parent() { 16 | let ref = this._ref.parent; 17 | if(!ref) { 18 | return null; 19 | } 20 | return this.store.doc(ref); 21 | } 22 | 23 | // 24 | 25 | doc(path) { 26 | let _ref; 27 | if(path) { 28 | _ref = this._ref.doc(path); 29 | } else { 30 | _ref = this._ref.doc(); 31 | } 32 | return this.store.doc(_ref); 33 | } 34 | 35 | // 36 | 37 | get dashboardURL() { 38 | let { store: { dashboardURL }, path } = this; 39 | return `${dashboardURL}/firestore/data/${path}`; 40 | } 41 | 42 | openDashboard() { 43 | window.open(this.dashboardURL, '_blank'); 44 | } 45 | 46 | // 47 | 48 | get string() { 49 | return this.path; 50 | } 51 | 52 | get serialized() { 53 | let { id, path } = this; 54 | return { id, path }; 55 | } 56 | 57 | toStringExtension() { 58 | return `${this.path}`; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /docs/decorators/model.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: model 3 | pos: 4 4 | --- 5 | 6 | # @model 7 | 8 | Model decorator creates model instance with provided name and properties. 9 | 10 | ``` javascript 11 | import Component from '@glimmer/component'; 12 | import { inject as service } from '@ember/service'; 13 | import { root, model } from 'zuglet/decorators'; 14 | 15 | @root() 16 | export default class NiceComponent extends Component { 17 | 18 | @service 19 | store 20 | 21 | @tracked 22 | type = 'message' 23 | 24 | @tracked 25 | id 26 | 27 | @model() 28 | .named(({ type }) => type) 29 | .mapping(({ store, id }) => ({ store, id })) 30 | .load(model => model.load()) // optional callback to load inner dependencies on 1st model activation 31 | model 32 | 33 | } 34 | ``` 35 | 36 | ``` javascript 37 | // models/message.js 38 | import EmberObject from '@ember/object'; 39 | export default class Message extends EmberObject { 40 | 41 | @tracked 42 | id 43 | 44 | @activate().content(({ store, id }) => store.doc(`messages/${id}`).existing()) 45 | doc 46 | 47 | // optional. invoked if @model().mapping(fn) properties has changed 48 | // if not provided, model is recreated on mapping changes 49 | mappingDidChange({ id }) { 50 | this.id = id; 51 | } 52 | 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getConfig = require('../../../config'); 4 | 5 | module.exports = function(environment) { 6 | let ENV = { 7 | modulePrefix: 'dummy', 8 | environment, 9 | rootURL: '/', 10 | locationType: 'history', 11 | EmberENV: { 12 | FEATURES: { 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | APP: { 20 | }, 21 | dummy: { 22 | firebase: getConfig('default').firebase, 23 | name: 'ember-cli-zuglet' 24 | }, 25 | fastboot: { 26 | hostWhitelist: [ /^localhost:\d+$/ ] 27 | } 28 | }; 29 | 30 | if(process.env.CI) { 31 | ENV.dummy.firebase = getConfig('travis').firebase; 32 | } 33 | 34 | console.log('Project:', ENV.dummy.firebase.projectId); 35 | 36 | if (environment === 'development') { 37 | // 38 | } 39 | 40 | if (environment === 'test') { 41 | ENV.locationType = 'none'; 42 | ENV.APP.LOG_ACTIVE_GENERATION = false; 43 | ENV.APP.LOG_VIEW_LOOKUPS = false; 44 | ENV.APP.rootElement = '#ember-testing'; 45 | ENV.APP.autoboot = false; 46 | } 47 | 48 | if (environment === 'production') { 49 | // 50 | } 51 | 52 | return ENV; 53 | }; 54 | -------------------------------------------------------------------------------- /blueprints/ember-cli-zuglet/files/__root__/store.js: -------------------------------------------------------------------------------- 1 | import Store from 'zuglet/store'; 2 | 3 | const options = { 4 | firebase: { 5 | apiKey: '', 6 | authDomain: '', 7 | databaseURL: '', 8 | projectId: '', 9 | storageBucket: '', 10 | messagingSenderId: '' 11 | }, 12 | firestore: { 13 | persistenceEnabled: true, 14 | // experimentalAutoDetectLongPolling: true, 15 | // experimentalForceLongPolling: true 16 | }, 17 | auth: { 18 | user: 'user' 19 | }, 20 | // functions: { 21 | // region: null 22 | // }, 23 | // emulators: { 24 | // host: 'localhost', 25 | // auth: 9099, 26 | // firestore: 8080, 27 | // storage: 9199, 28 | // functions: 5001 29 | // } 30 | }; 31 | 32 | if(!options.firebase.projectId) { 33 | // eslint-disable-next-line no-console 34 | console.log([ 35 | '', 36 | '🔥', 37 | '', 38 | 'No Firebase config provided.', 39 | 'Get your Firebase project configuration from https://console.firebase.google.com/', 40 | 'and paste it in the `app/store.js`', 41 | '', 42 | '' 43 | ].join('\n')); 44 | 45 | throw new Error('No firebase config provided in app/store.js'); 46 | } 47 | 48 | export default class <%= classifiedPackageName %>Store extends Store { 49 | 50 | options = options 51 | 52 | } 53 | -------------------------------------------------------------------------------- /addon/-private/util/types.js: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/compat/app"; 2 | import { isArray } from '@ember/array'; 3 | 4 | export { 5 | isArray 6 | } 7 | 8 | let _serverTimestamp; 9 | export const isServerTimestamp = arg => { 10 | if(!_serverTimestamp) { 11 | _serverTimestamp = firebase.firestore.FieldValue.serverTimestamp(); 12 | } 13 | return !!arg && typeof arg === 'object' && _serverTimestamp.isEqual(arg); 14 | }; 15 | 16 | export const isTimestamp = arg => arg instanceof firebase.firestore.Timestamp; 17 | export const isDocumentReference = arg => arg instanceof firebase.firestore.DocumentReference; 18 | export const isCollectionReference = arg => arg instanceof firebase.firestore.CollectionReference; 19 | export const isGeoPoint = arg => arg instanceof firebase.firestore.GeoPoint; 20 | export const isFirestoreBlob = arg => arg instanceof firebase.firestore.Blob; 21 | 22 | export const isFunction = arg => typeof arg === 'function'; 23 | 24 | const hasFileList = () => ('FileList' in window); 25 | const hasFile = () => ('File' in window); 26 | 27 | export const isDate = arg => arg instanceof Date; 28 | export const isFileList = arg => hasFileList() && arg instanceof FileList; 29 | export const isFile = arg => hasFile() && arg instanceof File; 30 | export const isPromise = arg => arg && isFunction(arg.then); 31 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/hljs/tomorrow.scss: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-quote { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-template-variable, 12 | .hljs-tag, 13 | .hljs-name, 14 | .hljs-selector-id, 15 | .hljs-selector-class, 16 | .hljs-regexp, 17 | .hljs-deletion { 18 | color: #c82829; 19 | } 20 | 21 | /* Tomorrow Orange */ 22 | .hljs-number, 23 | .hljs-built_in, 24 | .hljs-builtin-name, 25 | .hljs-literal, 26 | .hljs-type, 27 | .hljs-params, 28 | .hljs-meta, 29 | .hljs-link { 30 | color: #f5871f; 31 | } 32 | 33 | /* Tomorrow Yellow */ 34 | .hljs-attribute { 35 | color: #eab700; 36 | } 37 | 38 | /* Tomorrow Green */ 39 | .hljs-string, 40 | .hljs-symbol, 41 | .hljs-bullet, 42 | .hljs-addition { 43 | color: #f5871f; 44 | } 45 | 46 | /* Tomorrow Blue */ 47 | .hljs-title, 48 | .hljs-section { 49 | color: #4271ae; 50 | } 51 | 52 | /* Tomorrow Purple */ 53 | .hljs-keyword, 54 | .hljs-selector-tag { 55 | color: #718c00; 56 | } 57 | 58 | .hljs { 59 | display: block; 60 | overflow-x: auto; 61 | background: white; 62 | color: #4d4d4c; 63 | padding: 0.5em; 64 | } 65 | 66 | .hljs-emphasis { 67 | font-style: italic; 68 | } 69 | 70 | .hljs-strong { 71 | font-weight: bold; 72 | } -------------------------------------------------------------------------------- /docs/decorators/activate.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: activate 3 | pos: 3 4 | --- 5 | 6 | # @activate 7 | 8 | Activate decorator activates and deactivates property value based on parent's state. 9 | 10 | ## @activate().content(fn) 11 | 12 | ``` javascript 13 | // models/message.js 14 | import EmberObject from '@ember/object'; 15 | import { inject as service } from '@ember/service'; 16 | import { activate } from 'zuglet/decorators'; 17 | 18 | export default class Message extends EmberObject { 19 | 20 | @service 21 | store 22 | 23 | @tracked 24 | id 25 | 26 | @activate().content(({ store, id }) => store.doc(`messages/${id}`).existing()) 27 | doc 28 | 29 | } 30 | ``` 31 | 32 | ## @activate() 33 | 34 | If you don't provide `content()`, `@activate` acts as a writable property which activates set value 35 | 36 | ``` javascript 37 | // models/message.js 38 | import EmberObject from '@ember/object'; 39 | import { inject as service } from '@ember/service'; 40 | import { activate } from 'zuglet/decorators'; 41 | 42 | export default class Message extends EmberObject { 43 | 44 | @service 45 | store 46 | 47 | @activate() 48 | doc 49 | 50 | update(id) { 51 | // previous doc value is deactivated 52 | // new doc value is activated 53 | this.doc = this.store.doc(`messages/${id}`).existing(); 54 | } 55 | 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /tests/dummy/app/models/messages.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from 'zuglet/object'; 2 | import { inject as service } from '@ember/service'; 3 | import { objectToJSON, toJSON, load } from 'zuglet/utils'; 4 | import { activate, models } from 'zuglet/decorators'; 5 | 6 | export default class Messages extends ZugletObject { 7 | 8 | @service store; 9 | 10 | get coll() { 11 | return this.store.collection('messages'); 12 | } 13 | 14 | @activate() 15 | .content(({ coll }) => coll.query()) 16 | query; 17 | 18 | @models() 19 | .source(({ query }) => query.content) 20 | .named(doc => { 21 | if(doc.data.name ==='first') { 22 | return 'fancy-message'; 23 | } 24 | return 'message'; 25 | }) 26 | .mapping(doc => ({ doc })) 27 | models; 28 | 29 | async load() { 30 | await load.cached(this.query); 31 | } 32 | 33 | async add(name) { 34 | let doc = this.store.collection('messages').doc().new({ name }); 35 | await doc.save(); 36 | return doc; 37 | } 38 | 39 | byId(id) { 40 | return this.models.find(model => model.id === id); 41 | } 42 | 43 | get serialized() { 44 | let { models } = this; 45 | return { 46 | models: objectToJSON(models) 47 | }; 48 | } 49 | 50 | toJSON() { 51 | let { serialized } = this; 52 | return toJSON(this, { serialized }); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/remark.scss: -------------------------------------------------------------------------------- 1 | .remark { 2 | > .root { 3 | @import "hljs/index"; 4 | @mixin header-code() { 5 | > code { 6 | font-family: $font-code; 7 | display: inline-block; 8 | font-size: 14px; 9 | } 10 | } 11 | h1 { 12 | font-weight: 400; 13 | font-size: 21px; 14 | margin: 40px 0 0 0; 15 | @include header-code(); 16 | } 17 | h2 { 18 | font-weight: 400; 19 | font-size: 18px; 20 | margin: 50px 0 0 0; 21 | @include header-code(); 22 | } 23 | h1 + h2 { 24 | margin-top: 40px; 25 | } 26 | h3 { 27 | font-weight: 400; 28 | font-size: 17px; 29 | margin: 48px 0 0 0; 30 | @include header-code(); 31 | } 32 | h2 + h3 { 33 | margin-top: 48px; 34 | } 35 | blockquote { 36 | margin: 0; 37 | border-left: 2px solid #ddd; 38 | padding: 0 0 0 10px; 39 | } 40 | li { 41 | padding: 0 0 5px 0; 42 | > p { 43 | margin: 0; 44 | } 45 | &:last-child { 46 | padding: 0; 47 | } 48 | } 49 | > *:first-child { 50 | margin-top: 15px; 51 | } 52 | code { 53 | font-family: $font-code; 54 | display: inline-block; 55 | font-weight: 600; 56 | } 57 | pre code { 58 | font-weight: 400; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /firebase/standalone/lib/setup.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const getConfig = require('../../../config'); 3 | 4 | const initializeAdmin = (config, name) => { 5 | let { firebase, serviceAccountKey } = config; 6 | let credential = admin.credential.cert(require(serviceAccountKey)); 7 | return admin.initializeApp(Object.assign({}, firebase, { credential }), name); 8 | }; 9 | 10 | class Context { 11 | 12 | constructor(fullName) { 13 | this.fullName = fullName; 14 | this.config = getConfig(fullName); 15 | this.admin = initializeAdmin(this.config, fullName); 16 | this.firestore = this.admin.firestore(); 17 | this.auth = this.admin.auth(); 18 | this.storage = this.admin.storage(); 19 | this.bucket = this.storage.bucket(); 20 | } 21 | 22 | } 23 | 24 | const argv = require('minimist')(process.argv.slice(2)); 25 | 26 | const run = fn => Promise.resolve(fn()).then(arg => arg, err => { 27 | console.error(err.stack); 28 | process.exit(-1); 29 | }); 30 | 31 | const withContext = (fullName, fn) => run(async () => { 32 | let context = new Context(fullName); 33 | let result = await fn(context); 34 | context.admin.delete(); 35 | return result; 36 | }); 37 | 38 | const exit = message => { 39 | console.error(message); 40 | process.exit(-1); 41 | } 42 | 43 | module.exports = { 44 | Context, 45 | run, 46 | withContext, 47 | argv, 48 | exit 49 | }; 50 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ember-cli-zuglet 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/api/stores/stats.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Stats 3 | pos: 0 4 | --- 5 | 6 | # Stats 7 | 8 | List of currently: 9 | 10 | * activated models, documents, queries, … 11 | * observed documents, queries, auth, tasks 12 | * pending promises 13 | 14 | ``` javascript 15 | import { getStores } from 'zuglet/utils'; 16 | 17 | let stats = getStores(this).stats; 18 | ``` 19 | 20 | ``` javascript 21 | let stats = store.stores.stats; 22 | ``` 23 | 24 | ## activated 25 | 26 | List of activated models, documents, queries, … 27 | 28 | Useful for debugging. 29 | 30 | ## observers 31 | 32 | List of documents, queries, auth state change and storage tasks being observed. 33 | 34 | Useful for debugging. 35 | 36 | ## promises 37 | 38 | List of currently running promises which are not yet settled. Each promise has `stats` property with `{ model, label }` for debugging. 39 | 40 | ``` javascript 41 | let promise = stats.promises[0]; 42 | promise.stats.model // → 43 | promise.stats.label // → 'load' 44 | ``` 45 | 46 | ## stalledPromises 47 | 48 | List of currently running promises that are past configured `stallThreshold`. See [Initialize/Flags](api/initialize) 49 | 50 | ``` javascript 51 | let promise = stats.stalledPromises[0]; 52 | promise.stats.model // → 53 | promise.stats.label // → 'snapshot' 54 | ``` 55 | 56 | ## async settle() 57 | 58 | Resolves when all currently running `stats.promises` settle. 59 | -------------------------------------------------------------------------------- /addon/-private/store/firestore/batch.js: -------------------------------------------------------------------------------- 1 | import ZugletObject from '../../../object'; 2 | import { isDocument } from './document'; 3 | import { isDocumentReference } from './references/document'; 4 | import { registerPromise } from '../../stores/stats'; 5 | import { assert } from '@ember/debug'; 6 | 7 | export default class Batch extends ZugletObject { 8 | 9 | store = null; 10 | _batch = null; 11 | _callbacks = []; 12 | 13 | constructor(owner, { store, _batch }) { 14 | super(owner); 15 | this.store = store; 16 | this._batch = _batch; 17 | } 18 | 19 | async commit() { 20 | try { 21 | await registerPromise(this, 'commit', true, this._batch.commit()); 22 | this._callbacks.forEach(hash => hash.resolve()); 23 | } catch(err) { 24 | this._callbacks.forEach(hash => hash.reject(err)); 25 | throw err; 26 | } 27 | return this.store; 28 | } 29 | 30 | _registerCallbacks(hash) { 31 | this._callbacks.push(hash); 32 | } 33 | 34 | save(arg, opts) { 35 | assert(`argument must be Document not '${arg}'`, isDocument(arg)); 36 | this._registerCallbacks(arg._batchSave(this._batch, opts)); 37 | return arg; 38 | } 39 | 40 | delete(arg) { 41 | assert(`argument must be Document or Document Reference not '${arg}'`, isDocument(arg) || isDocumentReference(arg)); 42 | this._registerCallbacks(arg._batchDelete(this._batch)); 43 | return arg; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, /* embroiderOptimized */ } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function() { 7 | return { 8 | scenarios: [ 9 | { 10 | name: 'ember-lts-3.24', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': '~3.24.3', 14 | }, 15 | }, 16 | }, 17 | { 18 | name: 'ember-lts-3.28', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': '~3.28.0', 22 | }, 23 | }, 24 | }, 25 | // { 26 | // name: 'ember-release', 27 | // npm: { 28 | // devDependencies: { 29 | // 'ember-source': await getChannelURL('release') 30 | // } 31 | // } 32 | // }, 33 | // { 34 | // name: 'ember-beta', 35 | // npm: { 36 | // devDependencies: { 37 | // 'ember-source': await getChannelURL('beta'), 38 | // }, 39 | // }, 40 | // }, 41 | // { 42 | // name: 'ember-canary', 43 | // npm: { 44 | // devDependencies: { 45 | // 'ember-source': await getChannelURL('canary'), 46 | // }, 47 | // }, 48 | // }, 49 | embroiderSafe(), 50 | // embroiderOptimized(), 51 | ], 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /docs/decorators/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: models 3 | pos: 5 4 | --- 5 | 6 | # @models 7 | 8 | Creates models based on some kind of source array. Useful for wrapping firestore documents in models. 9 | 10 | ``` javascript 11 | import Component from '@glimmer/component'; 12 | import { inject as service } from '@ember/service'; 13 | import { root, activate, models } from 'zuglet/decorators'; 14 | import { action } from '@ember/object'; 15 | 16 | @root() 17 | export default class MessagesComponent extends Component { 18 | 19 | @service 20 | store 21 | 22 | // fetches documents and subscribes to query.onSnapshot 23 | @activate().content(({ store }) => store.collection('messages').query()) 24 | query 25 | 26 | // handles documents in `query.content` 27 | // creates, updates and removes models based on source 28 | @models() 29 | .source(({ query }) => query.content) 30 | .named(doc => doc.data.type) // if name changes, model is recreated 31 | .mapping(doc => ({ doc })) // if mapping changes, model.mappingDidChange is invoked or model is recreated 32 | .load(model => model.load()) // optional callback to load inner dependencies on 1st model activation 33 | models 34 | 35 | @action 36 | async add() { 37 | let ref = this.store.collection('messages').doc(); 38 | let doc = ref.new({ 39 | type: 'message', 40 | name: 'new message' 41 | }); 42 | await doc.save(); 43 | } 44 | 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /tests/integration/components-stats-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupRenderingStoreTest } from '../helpers/setup'; 2 | import { render } from '@ember/test-helpers'; 3 | import { hbs } from 'ember-cli-htmlbars'; 4 | 5 | module('components / stats', function(hooks) { 6 | setupRenderingStoreTest(hooks, false); 7 | 8 | test('render', async function(assert) { 9 | await render(hbs``); 10 | 11 | let el = this.element.querySelector('.zuglet-stats'); 12 | assert.ok(el); 13 | 14 | let sections = el.querySelectorAll('.section'); 15 | assert.strictEqual(sections.length, 4); 16 | 17 | let sectionInfo = section => { 18 | let label = section.querySelector('.label')?.innerText; 19 | let models = [ ...section.querySelectorAll('.models > .model') ].map(el => el.innerText); 20 | return { 21 | label, 22 | models 23 | }; 24 | } 25 | 26 | { 27 | let section = sectionInfo(sections[0]); 28 | assert.strictEqual(section.label, undefined); 29 | assert.strictEqual(section.models.length, 1); 30 | assert.ok(section.models[0].startsWith(' path.resolve(path.join(__dirname, '..', 'service-account-keys', `${name}.json`)); 5 | 6 | const configs = { 7 | default: { 8 | firebase: { 9 | apiKey: "AIzaSyDlYqLJJYWK7cdYBAtkZR5efA8HoYvcd6I", 10 | authDomain: "ember-cli-zuglet.firebaseapp.com", 11 | databaseURL: "https://ember-cli-zuglet.firebaseio.com", 12 | projectId: "ember-cli-zuglet", 13 | storageBucket: "ember-cli-zuglet.appspot.com", 14 | messagingSenderId: "337740781111", 15 | appId: "1:337740781111:web:d599271545ea7f2ff751b2" 16 | }, 17 | serviceAccountKey: serviceAccountKey('ember-cli-zuglet') 18 | }, 19 | travis: { 20 | firebase: { 21 | apiKey: "AIzaSyDoUTp48KAjzcRLRhf1AofFdrsHI6KujHw", 22 | authDomain: "ember-cli-zuglet-travis.firebaseapp.com", 23 | databaseURL: "https://ember-cli-zuglet-travis.firebaseio.com", 24 | projectId: "ember-cli-zuglet-travis", 25 | storageBucket: "ember-cli-zuglet-travis.appspot.com", 26 | messagingSenderId: "1053333094712", 27 | appId: "1:1053333094712:web:8e2aa84a201069524581cd" 28 | }, 29 | serviceAccountKey: serviceAccountKey('ember-cli-zuglet-travis') 30 | } 31 | }; 32 | 33 | const getConfig = name => { 34 | let config = configs[name]; 35 | assert(config, `Config for name '${name}' was not found`); 36 | return config; 37 | } 38 | 39 | module.exports = getConfig; 40 | -------------------------------------------------------------------------------- /firebase/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | /export/ 69 | /public/ 70 | -------------------------------------------------------------------------------- /addon/-private/model/decorators/route.js: -------------------------------------------------------------------------------- 1 | import { activate } from '../properties/activate'; 2 | import { getState } from '../state'; 3 | import { isFunction } from '../../util/types'; 4 | import { registerPromise } from '../../stores/stats'; 5 | 6 | const activateRoute = route => { 7 | if(!route.isActivated) { 8 | route.isActivated = true; 9 | getState(route).activate(route); 10 | } 11 | } 12 | 13 | const deactivateRoute = route => { 14 | if(route.isActivated) { 15 | route.isActivated = false; 16 | getState(route).deactivate(route); 17 | } 18 | route.active = null; 19 | } 20 | 21 | const extend = Class => class ActivatingRoute extends Class { 22 | 23 | @activate() 24 | active 25 | 26 | isActivated = false; 27 | 28 | async model(_, transition) { 29 | activateRoute(this); 30 | try { 31 | let model = await super.model(...arguments); 32 | this.active = model; 33 | if(!transition.isAborted && isFunction(this.load)) { 34 | await registerPromise(this, 'load', false, this.load(model)); 35 | } 36 | if(transition.isAborted) { 37 | deactivateRoute(this); 38 | } 39 | return model; 40 | } catch(err) { 41 | deactivateRoute(this); 42 | throw err; 43 | } 44 | } 45 | 46 | resetController(_, isExiting) { 47 | if(isExiting) { 48 | deactivateRoute(this); 49 | } 50 | return super.resetController(...arguments); 51 | } 52 | 53 | } 54 | 55 | export const route = () => Class => extend(Class); 56 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/models.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | import { setGlobal, toString } from 'zuglet/utils'; 4 | import { root, activate, models } from 'zuglet/decorators'; 5 | import { action } from '@ember/object'; 6 | import { dedupeTracked } from 'tracked-toolbox'; 7 | 8 | @root() 9 | export default class RouteModelsComponent extends Component { 10 | 11 | @service 12 | store 13 | 14 | @dedupeTracked 15 | name 16 | 17 | @dedupeTracked 18 | modelName = 'message' 19 | 20 | @activate() 21 | .content(({ store, name }) => { 22 | let ref = store.collection('messages'); 23 | if(name) { 24 | ref = ref.where('name', '==', name); 25 | } 26 | return ref.query(); 27 | }) 28 | query 29 | 30 | @models() 31 | .source(({ query }) => query.content) 32 | .named((doc, { modelName }) => doc.data.name === 'first' ? 'message' : modelName) 33 | .mapping(doc => ({ doc })) 34 | models 35 | 36 | constructor() { 37 | super(...arguments); 38 | setGlobal({ component: this }); 39 | } 40 | 41 | @action 42 | async add() { 43 | await this.store.collection('messages').doc().new({ 44 | name: 'new message' 45 | }).save(); 46 | } 47 | 48 | @action 49 | toggleModelName() { 50 | this.modelName = this.modelName === 'message' ? 'fancy-message' : 'message'; 51 | } 52 | 53 | toString() { 54 | return toString(this); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit/functions-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest } from '../helpers/setup'; 2 | 3 | module('functions', function(hooks) { 4 | setupStoreTest(hooks); 5 | 6 | test('call', async function(assert) { 7 | let result = await this.store.functions.call('echo', { ok: true }); 8 | assert.deepEqual(result, { 9 | data: { 10 | data: { 11 | ok: true 12 | }, 13 | uid: null 14 | } 15 | }); 16 | }); 17 | 18 | test('region returns default region', function(assert) { 19 | let region = this.store.functions.region(); 20 | assert.strictEqual(region.identifier, 'us-central1'); 21 | }); 22 | 23 | test('custom region', function(assert) { 24 | let region = this.store.functions.region('duckland-north1'); 25 | assert.strictEqual(region.identifier, 'duckland-north1'); 26 | }); 27 | 28 | test('serialized', function(assert) { 29 | assert.deepEqual(this.store.functions.serialized, { 30 | region: 'us-central1' 31 | }); 32 | }); 33 | 34 | test('toJSON', function(assert) { 35 | let json = this.store.functions.toJSON(); 36 | assert.deepEqual(json, { 37 | instance: json.instance, 38 | serialized: { 39 | region: 'us-central1' 40 | } 41 | }); 42 | assert.ok(json.instance.startsWith('zuglet@store/functions::ember')); 43 | }); 44 | 45 | test('toStringExtension', function(assert) { 46 | assert.strictEqual(this.store.functions.toStringExtension(), 'us-central1'); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | // 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: {}, 23 | overrides: [ 24 | // node files 25 | { 26 | files: [ 27 | './.eslintrc.js', 28 | './.prettierrc.js', 29 | './.template-lintrc.js', 30 | './ember-cli-build.js', 31 | './index.js', 32 | './testem.js', 33 | './blueprints/*/index.js', 34 | './config/**/*.js', 35 | './tests/dummy/config/**/*.js', 36 | './config.js' 37 | ], 38 | parserOptions: { 39 | sourceType: 'script', 40 | }, 41 | env: { 42 | browser: false, 43 | node: true, 44 | }, 45 | plugins: ['node'], 46 | extends: ['plugin:node/recommended'], 47 | }, 48 | { 49 | // test files 50 | files: ['tests/**/*-test.{js,ts}'], 51 | extends: ['plugin:qunit/recommended'], 52 | rules: { 53 | 'qunit/require-expect': 'off', 54 | 'qunit/no-ok-equality': 'off', 55 | 'qunit/no-negated-ok': 'off', 56 | 'qunit/no-assert-equal-boolean': 'off' 57 | } 58 | }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/storage.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 | 8 |
9 |
10 | {{if this.url this.url 'No url'}} 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | {{#if this.metadata}} 20 | 21 | {{else}} 22 | No metadata 23 | {{/if}} 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 |
33 | 34 | {{#if this.file}} 35 |
36 | 37 |
38 |
39 | 40 |
41 | {{/if}} 42 | 43 | {{#if this.task}} 44 | {{#if this.task.isCompleted}} 45 |
46 | Uploaded successfully 47 |
48 | {{else if this.task.isRunning}} 49 |
50 | Uploading {{this.task.progress}}%… 51 |
52 | {{/if}} 53 |
54 | 55 |
56 | {{/if}} 57 | 58 |
-------------------------------------------------------------------------------- /docs/api/auth/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: User 3 | pos: 0 4 | --- 5 | 6 | # User 7 | 8 | Currently signed in user. 9 | 10 | See [Store](api/store) `options.auth.user` on how to provide custom `User` implementation 11 | 12 | ## user 13 | 14 | Firebase Auth User instance 15 | 16 | ## props 17 | 18 | aliases of `this.user[key]` 19 | 20 | * uid 21 | * email 22 | * emailVerified 23 | * photoURL 24 | * displayName 25 | * isAnonymous 26 | 27 | ## restore(user) 28 | 29 | Override to load additional data on user sign-up/sign-in. 30 | 31 | ``` javascript 32 | import User from 'zuglet/user'; 33 | import { load } from 'zuglet/util'; 34 | 35 | export default class User extends BaseUser { 36 | 37 | @activate().content(({ store, uid }) => store.doc(`users/${uid}`).existing()) 38 | doc 39 | 40 | async restore(user) { 41 | super.restore(user); 42 | await load(this.doc); 43 | } 44 | 45 | } 46 | ``` 47 | 48 | ## signOut() 49 | 50 | Alias of `store.auth.signOut()` 51 | 52 | ## token({ type, refresh }) 53 | 54 | Fetch user's token. 55 | 56 | * `type` → defaults to `string`. `string` or `json` 57 | * `refresh` → defaults to `false` 58 | 59 | ``` javascript 60 | let token = store.auth.user.token({ type: 'json' }); 61 | ``` 62 | 63 | ## link(method, ...args) 64 | 65 | Link user accounts. 66 | 67 | * `method` → string 68 | * `...args` → arguments forwarded to method's credentials 69 | 70 | ``` javascript 71 | await store.auth.user.link('email', 'email@address.com', 'heythere') 72 | ``` 73 | 74 | ## async updatePassword(newPassword) `→ undefined` 75 | 76 | ``` javascript 77 | await store.auth.user.updatePassword(newPassword); 78 | ``` 79 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | {{content-for "head"}} 19 | 20 | 21 | 22 | 23 | {{content-for "head-footer"}} 24 | 25 | 26 | {{content-for "body"}} 27 | 28 | 29 | 30 | 31 | {{content-for "body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
ember install {{this.config.name}}
6 |
Ember.js Octane addon for effortless Firebase integration.
7 |
8 | 9 | 10 | Documentation 11 | Experiments 12 | GitHub 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{!-- 20 | 21 | --}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

33 | This addon is built and maintained by Arnis Vuskans, 34 | contact me at ampatspell@gmail.com for Ember.js and Firebase consulting. 35 |

36 |
37 | 38 |
-------------------------------------------------------------------------------- /addon/-private/model/properties/activate/activate.js: -------------------------------------------------------------------------------- 1 | import Property from '../property'; 2 | import { assert } from '@ember/debug'; 3 | import { isArray } from '@ember/array'; 4 | import ObjectActivator from './activators/object'; 5 | import ArrayActivator from './activators/array'; 6 | 7 | export default class BaseActivateProperty extends Property { 8 | 9 | _activatorTypeForValue(value) { 10 | if(isArray(value)) { 11 | return 'array'; 12 | } else { 13 | return 'object'; 14 | } 15 | } 16 | 17 | activatorTypeForValue(value) { 18 | let type = this._activatorTypeForValue(value); 19 | let { activator } = this; 20 | if(activator && value === null || value === undefined) { 21 | return activator.type; 22 | } 23 | return type; 24 | } 25 | 26 | createActivator(value) { 27 | if(isArray(value)) { 28 | return new ArrayActivator(this, value); 29 | } 30 | return new ObjectActivator(this, value); 31 | } 32 | 33 | assertActivatorType(activator, value) { 34 | assert([ 35 | `Changing activator type is not supported.`, 36 | `Now property '${this.key}' for ${this.owner} is of type ${activator.type}`, 37 | `but value '${value}' asks for activator of type ${this.activatorTypeForValue(value)}` 38 | ].join(' '), activator.type === this.activatorTypeForValue(value)); 39 | } 40 | 41 | // 42 | 43 | onActivated() { 44 | let { activator } = this; 45 | if(activator) { 46 | activator.activate(); 47 | } 48 | } 49 | 50 | onDeactivated() { 51 | let { activator } = this; 52 | if(activator) { 53 | activator.deactivate(); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /addon/-private/util/error.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from './types'; 2 | 3 | const { 4 | assign 5 | } = Object; 6 | 7 | export class ZugletError extends Error { 8 | 9 | get serialized() { 10 | return Object.getOwnPropertyNames(this).reduce((hash, key) => { 11 | hash[key] = this[key]; 12 | return hash; 13 | }, {}); 14 | } 15 | 16 | toJSON() { 17 | let { serialized } = this; 18 | return { type: 'zuglet-error', serialized }; 19 | } 20 | 21 | } 22 | 23 | export const error = opts => { 24 | let { message, code } = opts; 25 | delete opts.message; 26 | delete opts.code; 27 | 28 | let err = new ZugletError(message); 29 | err.code = `zuglet/${code}`; 30 | assign(err, opts); 31 | 32 | return err; 33 | } 34 | 35 | export const documentForRefNotFoundError = ref => error({ 36 | message: `Document '${ref.path}' missing`, 37 | code: 'document/missing', 38 | path: ref.path 39 | }); 40 | 41 | export const documentNotFoundError = () => error({ 42 | message: `Document missing`, 43 | code: 'document/missing' 44 | }); 45 | 46 | export const cancelledError = () => error({ 47 | message: 'Snapshot cancelled', 48 | code: 'snapshot/cancelled' 49 | }); 50 | 51 | export const timeoutError = () => error({ 52 | message: 'Snapshot timeout', 53 | code: 'snapshot/timeout' 54 | }); 55 | 56 | export const assert = (message, condition) => { 57 | if(!condition) { 58 | if(isFunction(message)) { 59 | message = message(); 60 | } 61 | throw error({ 62 | message, 63 | code: 'assert' 64 | }); 65 | } 66 | }; 67 | 68 | export const isZugletError = err => { 69 | return err instanceof ZugletError; 70 | }; 71 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/route/index.scss: -------------------------------------------------------------------------------- 1 | .route-index { 2 | margin: 80px 50px 33px 50px; 3 | max-width: 800px; 4 | @include breakpoint(tablet) { 5 | margin: 50px 40px; 6 | } 7 | @include breakpoint(mobile) { 8 | margin: 25px 20px; 9 | } 10 | } 11 | 12 | .block-index-section { 13 | margin-bottom: 33px; 14 | 15 | display: flex; 16 | flex-direction: row; 17 | align-items: baseline; 18 | 19 | > .aside { 20 | font-size: $font-size-smaller; 21 | text-transform: uppercase; 22 | width: 70px; 23 | opacity: 0.3; 24 | } 25 | 26 | > .content { 27 | flex: 1; 28 | overflow-x: hidden; 29 | } 30 | 31 | @include breakpoint(mobile) { 32 | > .aside { 33 | display: none; 34 | } 35 | } 36 | 37 | &.header { 38 | > .content { 39 | > .fire { 40 | font-size: 32px; 41 | line-height: 1; 42 | &:after { 43 | content: '🔥'; 44 | } 45 | @include breakpoint(non-desktop) { 46 | margin-bottom: 10px; 47 | } 48 | } 49 | > .header { 50 | font-size: $font-size * 2; 51 | > .value { 52 | font-weight: 600; 53 | } 54 | @include breakpoint(mobile) { 55 | > .prefix { 56 | display: none; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | &.links { 64 | > .content { 65 | display: flex; 66 | flex-direction: row; 67 | font-weight: 600; 68 | > a { 69 | margin-right: 10px; 70 | } 71 | } 72 | } 73 | 74 | &.author { 75 | > .content { 76 | > p { 77 | margin-bottom: 0; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /addon/-private/store/storage/storage.js: -------------------------------------------------------------------------------- 1 | import 'firebase/compat/storage'; 2 | import ZugletObject from '../../../object'; 3 | import { toJSON } from '../../util/to-json'; 4 | import { getFactory } from '../../factory/get-factory'; 5 | 6 | export default class Storage extends ZugletObject { 7 | 8 | constructor(owner, { store }) { 9 | super(owner); 10 | this.store = store; 11 | this._maybeSetupEmulator(); 12 | } 13 | 14 | _maybeSetupEmulator() { 15 | let emulators = this.store.normalizedOptions.emulators; 16 | if(emulators.storage) { 17 | this._storage.useEmulator(emulators.storage.host, emulators.storage.port); 18 | } 19 | } 20 | 21 | get _storage() { 22 | return this.store.firebase.storage(); 23 | } 24 | 25 | get bucket() { 26 | return this._storage.app.options.storageBucket; 27 | } 28 | 29 | _createReference(_ref) { 30 | let storage = this; 31 | return getFactory(this).zuglet.create('store/storage/reference', { storage, _ref }); 32 | } 33 | 34 | ref(arg) { 35 | if(typeof arg === 'string') { 36 | arg = { path: arg }; 37 | } 38 | 39 | let { path, url } = arg; 40 | let _ref; 41 | if(path) { 42 | _ref = this._storage.ref(path); 43 | } else { 44 | _ref = this._storage.refFromURL(url); 45 | } 46 | 47 | return this._createReference(_ref); 48 | } 49 | 50 | get serialized() { 51 | let { bucket } = this; 52 | return { 53 | bucket 54 | }; 55 | } 56 | 57 | toJSON() { 58 | let { serialized } = this; 59 | return toJSON(this, { serialized }); 60 | } 61 | 62 | toStringExtension() { 63 | return `${this.bucket}`; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "12" 7 | 8 | dist: xenial 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | directories: 15 | - $HOME/.npm 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | branches: 23 | only: 24 | - master 25 | # npm version tags 26 | - /^v\d+\.\d+\.\d+/ 27 | 28 | jobs: 29 | fast_finish: true 30 | allow_failures: 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | - stage: "Tests" 36 | name: "Tests" 37 | script: 38 | - npm run lint 39 | - npm run test:ember 40 | 41 | - stage: "Additional Tests" 42 | name: "Floating Dependencies" 43 | install: 44 | - npm install --no-package-lock 45 | script: 46 | - npm run test:ember 47 | 48 | # we recommend new addons test the current and previous LTS 49 | # as well as latest stable release (bonus points to beta/canary) 50 | - env: EMBER_TRY_SCENARIO=ember-lts-3.20 51 | - env: EMBER_TRY_SCENARIO=ember-lts-3.24 52 | - env: EMBER_TRY_SCENARIO=ember-release 53 | - env: EMBER_TRY_SCENARIO=ember-beta 54 | - env: EMBER_TRY_SCENARIO=ember-canary 55 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 56 | - env: EMBER_TRY_SCENARIO=ember-classic 57 | - env: EMBER_TRY_SCENARIO=embroider-safe 58 | - env: EMBER_TRY_SCENARIO=embroider-optimized 59 | 60 | script: 61 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 62 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/storage.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { inject as service } from '@ember/service'; 3 | import { setGlobal, toString } from 'zuglet/utils'; 4 | import { root, activate } from 'zuglet/decorators'; 5 | import { action } from '@ember/object'; 6 | import { tracked } from '@glimmer/tracking'; 7 | 8 | @root() 9 | export default class RouteStorageComponent extends Component { 10 | 11 | @service 12 | store 13 | 14 | @tracked 15 | file 16 | 17 | @tracked 18 | url 19 | 20 | @tracked 21 | metadata 22 | 23 | @activate() 24 | task 25 | 26 | constructor() { 27 | super(...arguments); 28 | setGlobal({ component: this }); 29 | } 30 | 31 | @action 32 | onFile(file) { 33 | this.file = file; 34 | } 35 | 36 | @action 37 | upload() { 38 | let { file } = this; 39 | this.file = null; 40 | let task = this.store.storage.ref('hello').put({ 41 | type: 'data', 42 | data: file, 43 | metadata: { 44 | contentType: file.type, 45 | contentDisposition: `filename="${file.name}"` 46 | } 47 | }); 48 | this.task = task; 49 | } 50 | 51 | @action 52 | async getUrl() { 53 | try { 54 | let url = await this.store.storage.ref('hello').url(); 55 | this.url = url; 56 | } catch(err) { 57 | this.url = err; 58 | } 59 | } 60 | 61 | @action 62 | async getMetadata() { 63 | try { 64 | let metadata = await this.store.storage.ref('hello').metadata(); 65 | this.metadata = metadata; 66 | } catch(err) { 67 | this.metadata = err; 68 | } 69 | } 70 | 71 | toString() { 72 | return toString(this); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /addon/-private/store/auth/methods/email.js: -------------------------------------------------------------------------------- 1 | import AuthMethod from './method'; 2 | import firebase from 'firebase/compat/app'; 3 | import { registerPromise } from '../../../stores/stats'; 4 | 5 | const { 6 | assign 7 | } = Object; 8 | 9 | export default class EmailAuthMethod extends AuthMethod { 10 | 11 | signIn(email, password) { 12 | return this.auth._withAuthReturningUser(async auth => { 13 | let { user } = await registerPromise(this, 'sign-in', true, auth.signInWithEmailAndPassword(email, password)); 14 | return { user }; 15 | }); 16 | } 17 | 18 | signUp(email, password) { 19 | return this.auth._withAuthReturningUser(async auth => { 20 | let { user } = await registerPromise(this, 'sign-up', true, auth.createUserWithEmailAndPassword(email, password)); 21 | return { user }; 22 | }); 23 | } 24 | 25 | sendSignInLink(email, opts) { 26 | opts = assign({ handleCodeInApp: true }, opts); 27 | return this.auth._withAuth(async auth => { 28 | await registerPromise(this, 'send-link', true, auth.sendSignInLinkToEmail(email, opts)); 29 | }); 30 | } 31 | 32 | signInWithLink(email, link) { 33 | return this.auth._withAuthReturningUser(async auth => { 34 | let { user } = await registerPromise(this, 'sign-in-with-link', true, auth.signInWithEmailLink(email, link)); 35 | return { user }; 36 | }); 37 | } 38 | 39 | credential(email, password) { 40 | return firebase.auth.EmailAuthProvider.credential(email, password); 41 | } 42 | 43 | sendPasswordReset(email, opts) { 44 | return this.auth._withAuth(auth => { 45 | return registerPromise(this, 'send-password-reset', true, auth.sendPasswordResetEmail(email, opts)); 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /addon/-private/util/object-to-json.js: -------------------------------------------------------------------------------- 1 | import { 2 | isTimestamp, 3 | isServerTimestamp, 4 | isGeoPoint, 5 | isFunction, 6 | isArray, 7 | isDate, 8 | isFile, 9 | isFileList, 10 | isFirestoreBlob 11 | } from './types'; 12 | 13 | import { 14 | formatDate 15 | } from './date'; 16 | 17 | export const objectToJSON = value => { 18 | if(typeof value === 'object') { 19 | if(value === null) { 20 | return value; 21 | } else if(isTimestamp(value)) { 22 | return { 23 | type: 'timestamp', 24 | value: formatDate(value.toDate()) 25 | }; 26 | } else if(isGeoPoint(value)) { 27 | let json = value.toJSON(); 28 | return { 29 | type: 'geopoint', 30 | ...json 31 | }; 32 | } else if(isFunction(value.toJSON)) { 33 | return value.toJSON(); 34 | } else if(isArray(value)) { 35 | return value.map(item => objectToJSON(item)); 36 | } else if(isDate(value)) { 37 | return { 38 | type: 'date', 39 | value: formatDate(value) 40 | }; 41 | } else if(isFile(value)) { 42 | let { name, type: contentType, size } = value; 43 | return { 44 | type: 'file', 45 | name, 46 | contentType, 47 | size 48 | }; 49 | } else if(isFileList(value)) { 50 | let files = [ ...value ]; 51 | return files.map(file => objectToJSON(file)); 52 | } else if(isServerTimestamp(value)) { 53 | return { 54 | type: 'server-timestamp', 55 | }; 56 | } else if(isFirestoreBlob(value)) { 57 | return { 58 | type: 'firestore-blob', 59 | }; 60 | } else { 61 | let hash = {}; 62 | Object.getOwnPropertyNames(value).forEach(key => { 63 | hash[key] = objectToJSON(value[key]); 64 | }); 65 | return hash; 66 | } 67 | } 68 | return value; 69 | }; 70 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | import { isFastBoot } from 'zuglet/-private/util/fastboot'; 4 | 5 | let { 6 | environment 7 | } = config; 8 | 9 | let isGoogleAnalyticsEnabled = environment === 'production'; 10 | 11 | export default class Router extends EmberRouter { 12 | location = config.locationType; 13 | rootURL = config.rootURL; 14 | 15 | constructor() { 16 | super(...arguments); 17 | this.on('routeDidChange', () => this.routeDidChange()); 18 | } 19 | 20 | sendPageview() { 21 | if(!isGoogleAnalyticsEnabled) { 22 | return; 23 | } 24 | if(typeof gtag_pageview !== 'undefined') { 25 | let url = this.currentURL; 26 | gtag_pageview(url); /* eslint-disable-line no-undef */ 27 | } 28 | } 29 | 30 | routeDidChange() { 31 | if(!isFastBoot(this)) { 32 | this.sendPageview(); 33 | } 34 | } 35 | 36 | } 37 | 38 | Router.map(function() { 39 | 40 | this.route('docs', function() { 41 | this.route('page', { path: '/*page_id' }, function() { 42 | }); 43 | }); 44 | 45 | this.route('playground', function() { 46 | this.route('document'); 47 | this.route('query', function() { 48 | this.route('array'); 49 | this.route('single'); 50 | }); 51 | this.route('models'); 52 | this.route('content'); 53 | this.route('route'); 54 | this.route('auth', function() { 55 | this.route('email', { path: 'email/:email' }); 56 | }); 57 | this.route('storage'); 58 | this.route('functions'); 59 | this.route('dev'); 60 | this.route('reordering'); 61 | this.route('messages', function() { 62 | this.route('message', { path: ':message_id' }, function() { 63 | }); 64 | }); 65 | }); 66 | 67 | this.route('missing', { path: '/*path' }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/body.scss: -------------------------------------------------------------------------------- 1 | html { 2 | padding: 0; 3 | margin: 0; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | padding: 0; 9 | margin: 0; 10 | font-family: $font-text; 11 | font-size: $font-size; 12 | line-height: 1.3; 13 | cursor: default; 14 | height: 100%; 15 | min-height: 100%; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | * { 21 | box-sizing: border-box; 22 | } 23 | 24 | #qunit { 25 | button { 26 | color: #000; 27 | background: #fff !important; 28 | border: 1px solid fade-out(#000, 0.75) !important; 29 | } 30 | } 31 | 32 | @mixin button() { 33 | font-family: $font-code; 34 | font-size: $font-size; 35 | background: fade-out(#000, 0.2); 36 | border-radius: 3px; 37 | border: none; 38 | color: #fff; 39 | padding: 5px 9px 5px 9px; 40 | outline: none; 41 | font-size: 14px; 42 | margin: 0; 43 | } 44 | 45 | a, 46 | a:visited, 47 | a:active { 48 | color: #000; 49 | text-decoration: underline; 50 | text-decoration-color: fade-out(#000, 0.8); 51 | transition: text-decoration-color 0.2s ease-in-out; 52 | &:hover { 53 | text-decoration-color: fade-out(#000, 0.1); 54 | } 55 | } 56 | 57 | button, 58 | input[type="button"] { 59 | @include button(); 60 | } 61 | 62 | button:disabled, 63 | input[type="button"]:disabled { 64 | color: #999; 65 | } 66 | 67 | input:not([type]), 68 | input[type="text"], 69 | input[type="number"], 70 | input[type="password"] { 71 | font-family: $font-code; 72 | font-size: $font-size; 73 | border-radius: 3px; 74 | border: 1px solid #999; 75 | padding: 4px 9px 4px 9px; 76 | margin: 0; 77 | line-height: 1; 78 | outline: none; 79 | } 80 | 81 | textarea { 82 | font-family: $font-code; 83 | font-size: $font-size; 84 | border-radius: 3px; 85 | border: 1px solid #999; 86 | padding: 5px; 87 | margin: 0; 88 | line-height: 1; 89 | outline: none; 90 | } 91 | -------------------------------------------------------------------------------- /tests/unit/auth-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, setupStoreTest, credentials } from '../helpers/setup'; 2 | 3 | module('auth', function(hooks) { 4 | setupStoreTest(hooks); 5 | 6 | hooks.beforeEach(async function() { 7 | this.auth = this.store.auth; 8 | await this.auth.signOut(); 9 | }); 10 | 11 | hooks.afterEach(async function() { 12 | await this.auth.signOut(); 13 | }); 14 | 15 | test('user delete', async function(assert) { 16 | let user = await this.auth.methods.anonymous.signIn(); 17 | await user.delete(); 18 | assert.strictEqual(this.auth.user, null); 19 | }); 20 | 21 | test('serialized', async function(assert) { 22 | assert.deepEqual(this.auth.serialized, { 23 | user: null 24 | }); 25 | }); 26 | 27 | test('serialized with signed in user', async function(assert) { 28 | await this.auth.methods.email.signIn(credentials.ampatspell.email, credentials.ampatspell.password); 29 | let { serialized } = this.auth; 30 | assert.deepEqual(serialized, { 31 | user: { 32 | instance: serialized.user.instance, 33 | serialized: { 34 | email: credentials.ampatspell.email, 35 | emailVerified: serialized.user.serialized.emailVerified, 36 | isAnonymous: false, 37 | uid: serialized.user.serialized.uid 38 | } 39 | } 40 | }); 41 | assert.ok(serialized.user.instance.startsWith('dummy@model:test-user::ember')); 42 | assert.ok(typeof serialized.user.serialized.emailVerified === 'boolean'); 43 | assert.ok(typeof serialized.user.serialized.uid === 'string'); 44 | }); 45 | 46 | test('toJSON', async function(assert) { 47 | let json = this.auth.toJSON(); 48 | assert.deepEqual(json, { 49 | instance: json.instance, 50 | serialized: { 51 | user: null 52 | } 53 | }); 54 | assert.ok(json.instance.startsWith('zuglet@store/auth::ember')); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /tests/dummy/app/models/docs/page.js: -------------------------------------------------------------------------------- 1 | import { setOwner } from '@ember/application'; 2 | import { reads } from "macro-decorators"; 3 | import { cached } from "tracked-toolbox"; 4 | import { remark } from 'remark/decorators'; 5 | 6 | const withoutExtension = fn => (target, key) => cached(target, key, { 7 | get() { 8 | let components = fn.call(this, this).split('.'); 9 | components.pop(); 10 | return components.join('.'); 11 | } 12 | }); 13 | 14 | export default class Page { 15 | 16 | constructor(owner, { docs, file }) { 17 | setOwner(this, owner); 18 | this.docs = docs; 19 | this.file = file; 20 | } 21 | 22 | @reads('file.attributes.pos') pos; 23 | @reads('file.attributes.hidden') hidden; 24 | @reads('file.body') body; 25 | @reads('file.filename') filename; 26 | @reads('file.directory') directory; 27 | 28 | @withoutExtension(page => page.file.name) id; 29 | @withoutExtension(page => page.filename) name; 30 | 31 | get pages() { 32 | return this.docs.directory(this.id); 33 | } 34 | 35 | @cached 36 | get title() { 37 | let { file } = this; 38 | return file.attributes.title || file.toc[0]?.content || this.name; 39 | } 40 | 41 | @remark('body') 42 | tree(node) { 43 | if(node.tagName === 'a') { 44 | let href = node.properties.href; 45 | if(href.startsWith('http:') || href.startsWith('https:') || href.startsWith('mailto:')) { 46 | node.properties.target = 'top'; 47 | } else if(href.startsWith('api/')) { 48 | return { 49 | type: 'component', 50 | name: 'block/remark/link-to', 51 | inline: true, 52 | model: { 53 | route: 'docs.page', 54 | model: href 55 | }, 56 | children: node.children 57 | }; 58 | } else { 59 | console.log('Unmapped link', node); 60 | } 61 | } 62 | return node; 63 | } 64 | 65 | async load() { 66 | await this.file.load(); 67 | await this.tree.load(); 68 | return this; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /docs/api/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Models 3 | pos: 2 4 | --- 5 | 6 | # Models 7 | 8 | A simple wrapper around Ember's `getOwner(this).getFactory(…)` which works only with `model:…` registrations. 9 | 10 | ``` javascript 11 | let models = store.models; 12 | ``` 13 | 14 | ## create(name, ...args) `→ instance` 15 | 16 | Creates a new model instance for given factory name and properties. 17 | 18 | ### EmberObject 19 | 20 | ``` javascript 21 | // app/models/message.js 22 | export default class Message extends EmberObject { 23 | } 24 | ``` 25 | 26 | ``` javascript 27 | let model = models.create('message', { from: 'larry', to: 'zeeba' }); 28 | model.from // → 'larry' 29 | model.to // → 'zeeba' 30 | ``` 31 | 32 | ### Plain ES6 Class 33 | 34 | ``` javascript 35 | // app/models/message.js 36 | import { setOwner } from '@ember/application'; 37 | 38 | export default class Message { 39 | constructor(owner, from, to) { 40 | setOwner(this, owner); // owner → `getOwner(this)` 41 | this.from = from; 42 | this.to = to; 43 | } 44 | } 45 | ``` 46 | 47 | ``` javascript 48 | let model = models.create('message', 'larry', 'zeeba'); 49 | model.from // → 'larry' 50 | model.to // → 'zeeba' 51 | ``` 52 | 53 | ## Plain ZugletObject ES6 class 54 | 55 | `ZugletObject` sets owner and overrides `toString()`. 56 | 57 | ``` javascript 58 | // app/models/message.js 59 | import ZugletObject from 'zuglet/object'; 60 | 61 | export default class Message { 62 | constructor(owner, from, to) { 63 | super(owner); 64 | this.from = from; 65 | this.to = to; 66 | } 67 | } 68 | ``` 69 | 70 | ``` javascript 71 | let model = models.create('message', 'larry', 'zeeba'); 72 | model.from // → 'larry' 73 | model.to // → 'zeeba' 74 | ``` 75 | 76 | ## factoryFor(name, { optional }) `→ factory or undefined` 77 | 78 | * `optional` → boolean, defaults to false 79 | 80 | Lookups model factory for name. 81 | 82 | If `optional` is false and factory is not registered, assertation error is thrown. 83 | 84 | ## registerFactory(name, factory) `→ undefined` 85 | 86 | Registers model factory 87 | -------------------------------------------------------------------------------- /addon/components/zuglet/stats.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if this.showStores}} 3 |
4 | {{#if this.hasSingleStore}} 5 |
6 |
{{this.firstStore}}
7 |
8 | {{else}} 9 |
Stores
10 |
11 | {{#each this.stores as |model|}} 12 |
{{model}}
13 | {{else}} 14 |
No Stores
15 | {{/each}} 16 |
17 | {{/if}} 18 |
19 | {{/if}} 20 |
21 |
Activated ({{this.stats.activated.length}})
22 |
23 | {{#each this.stats.activated as |model|}} 24 |
{{model}}
25 | {{else}} 26 |
No activated models
27 | {{/each}} 28 |
29 |
30 |
31 |
Observers ({{this.stats.observers.length}})
32 |
33 | {{#each this.stats.observers as |model|}} 34 |
{{model}}
35 | {{else}} 36 |
No onSnapshot listeners
37 | {{/each}} 38 |
39 |
40 |
41 |
Promises ({{this.stats.promises.length}})
42 |
43 | {{#each this.stats.promises as |promise|}} 44 |
{{promise.stats.label}} : {{promise.stats.model}}
45 | {{else}} 46 |
No promises
47 | {{/each}} 48 |
49 |
50 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-cli-zuglet ![CI](https://github.com/ampatspell/ember-cli-zuglet/workflows/CI/badge.svg) [![npm version](https://img.shields.io/npm/v/ember-cli-zuglet.svg)](https://badge.fury.io/js/ember-cli-zuglet) 2 | 3 | This addon is dead simple way to use Google Firebase services in your Ember.js apps. Cloud Firestore, Storage, Auth, Functions. 4 | 5 | **[ember-cli-zuglet is built and maintained by Arnis Vuskans, contact me for Ember.js, ember-cli-zuglet and Google Firebase consulting](https://www.amateurinmotion.com/)**. 6 | 7 | ember-cli-zuglet@^2.0.0 is a complete rewrite for Ember.js Octane edition. 8 | 9 | * [Website](https://www.ember-cli-zuglet.com/) 10 | * [Documentation](https://www.ember-cli-zuglet.com/docs) 11 | 12 | Open source apps built using ember-cli-zuglet: 13 | 14 | * [kaste](https://github.com/ampatspell/kaste) 15 | * [dzeja](https://github.com/ampatspell/dzeja) 16 | * Pre-octane Ember.js & ember-cli-zuglet v1.x 17 | * [tiny](http://github.com/ampatspell/tiny) 18 | * [bain ×](https://getbain.com/) 19 | * [index65](https://github.com/ampatspell/index65) 20 | * [ohne-zeit](https://github.com/ampatspell/ohne-zeit) 21 | 22 | ## Install 23 | 24 | ``` bash 25 | $ ember new foof --skip-npm 26 | ``` 27 | 28 | ``` diff 29 | "devDependencies": { 30 | - "ember-data": "~3.22.0", 31 | } 32 | ``` 33 | 34 | ``` bash 35 | $ ember install ember-cli-zuglet 36 | ``` 37 | 38 | Open `app/store.js` and add your Firebase config. 39 | 40 | ## Documentation 41 | 42 | See the [ember-cli-zuglet website](https://www.ember-cli-zuglet.com) for documentation and [dummy app](https://github.com/ampatspell/ember-cli-zuglet/tree/master/tests/dummy/app/components/route/playground) for examples. 43 | 44 | ## Tweaks 45 | 46 | ``` javascript 47 | // jsconfig.json 48 | { 49 | "compilerOptions": { 50 | "target": "es6", 51 | "experimentalDecorators": true 52 | }, 53 | "exclude": [ "node_modules", ".git" ] 54 | } 55 | ``` 56 | 57 | ## Other useful addons to install 58 | 59 | ``` bash 60 | $ ember install tracked-toolbox 61 | $ ember install macro-decorators 62 | ``` 63 | -------------------------------------------------------------------------------- /tests/dummy/app/components/route/playground/auth.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{#if this.error}} 4 |
5 |
Error
6 |
{{this.error}}
7 |
8 | {{/if}} 9 | 10 | {{#if this.store.auth.user}} 11 |
12 | 13 |
14 | {{#if this.store.auth.user.photoURL}} 15 |
16 | profile pic 17 |
18 | {{/if}} 19 | {{else}} 20 |
21 | Signed out 22 |
23 | {{/if}} 24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 | {{#if this.store.auth.user.isAnonymous}} 32 | 33 | {{/if}} 34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 |
--------------------------------------------------------------------------------