swoof Svelte Themes

Swoof

🔥 Google Firebase Firestore, Auth, Storage, Functions library for Svelte.

Swoof

Swoof is Google Firebase Firestore, Auth, Storage, Functions library for Svelte.

Proof of concept

See /dummy for some examples.

Setting up

$ npm install swoof --save-dev
// App.svete
<script>
  import { swoof, state, setGlobal, User } from 'swoof';
  import SomethingNice from './SomethingNice.svelte';

  class FancyUser extends User {
  }

  let { firebase } = process.env.CONFIG;

  let config = {
    firebase: {
      apiKey: "...",
      authDomain: "...",
      databaseURL: "...",
      projectId: "...",
      storageBucket: "...",
      messagingSenderId: "...",
      appId: "..."
    },
    firestore: {
      enablePersistence: true
    },
    swoof: {
      auth: {
        User: FancyUser
      },
      // override default region for store.functions
      // functions: {
      //   region: 'us-central1'
      // }
    }
  };

  // internally creates FirebaseApp named main
  swoof.configure('main', config);

  // creates store named `main` using firebase app named `main`
  // swoof supports multiple firebase apps
  let store = swoof.create('main', 'main');

  // Optional tools for playing around in console
  setGlobal({ store });
  setGlobal({ state });
</script>

<SomethingNice/>

<style>
</style>
// console
await store.doc('message/hello').new({ text: 'hey there' }).save();

If you're getting weird build or runtime errors, see below.

API

swoof

import { swoof } from 'swoof';

configure(name, config): undefined

Creates FirebaseApp and links it to the name.

create(identifier, name): store

Creates and returns swoof store with given identifier and configuration name.

store(identifier): store or undefined

Returns existing store for identifier.

swoof.create('main', 'production'); // once

// somewhere else
let store = swoof.store('main');

destroy(): undefined

Destroys internal FirebaseApp instances

Model

Soon. See /dummy for examples

// lib/messages.js
import { Model, properties } from 'swoof';

const {
  attr,
  models,
  tap
} = properties;

class Message extends Model {

  constructor(message) {
    super();
    // tap doesn't bind, just forwards change notifications in this context
    this.property('doc', tap(doc));
  }

  get data() {
    return this.doc.data;
  }

  get text() {
    return this.data.text;
  }

  async save() {
    await this.doc.save();
  }

}

export default class Messages extends Model {

  constructor(store) {
    super();
    this.store = this;
    this.coll = store.collection('messages');

    // query autosubscribes to ref.onSnapshot
    this.property('query', attr(this.coll.orderBy('createdAt').query()));

    // Message models are automatically created for each document.
    // then added/removed based on snapshot.docChanges
    this.property('messages', models('query.content', doc => new Message(doc)));
  }

  async add(text) {
    let { store } = this;
    let doc = this.coll.doc().new({
      text,
      createdAt: store.serverTimestamp();
    });
    await doc.save();
  }

}
<script>
  import { store } from 'swoof';
  import Messages from './lib/messages';

  // Writable when subscribed starts all onSnapshot listeners and
  // property change notifications
  // Everything is torn down when last subscriber unsubscribes.
  let messages = writable(new Messages(store));
</script>

<!-- use "$" only for `messages` - first level -->

<div>{$messages.count} messages.</div>
<div>
  {#each $messages.message as message}
    <div>{message.text}</div>
  {/each}
</div>

writable(model): svelte/writable

Creates Svelte writable for sfoof model instance or tree.

Properties

  • attr
  • array
  • models
  • tap
  • alias
  • logger

load()

await load(....modelsOrPromises);

Store

import { swoof } from 'swoof';
let store = swoof.store('main');

doc(path): DocumentReference

Creates swoof firestore document reference.

let ref = store.doc('messages/first');

collection(path): CollectionReference

Creates swoof firestore collection reference.

let ref = store.doc('messages/first/comments');

serverTimestamp(): firestore.FieldValue.ServerTimestamp

let doc = store.doc('messages/first').new({
  text: 'hey there',
  createdAt: store.serverTimestamp()
});
await doc.save();

DocumentReference

let ref = store.doc('messages/first');
let ref = store.collection('messages').doc('first');
let ref = store.collection('messages').doc(); // generated id

id: string

Document id

path: string

Document path

collection(path): CollectionReference

Creates nested Collection Reference

let coll = store.doc('messages/first').collection('comments');

new(props): Document

Creates Document instance which is not automatically subscribed to onSnapshot listener.

Subscription to onSnapshot happens right after save or load.

let doc = store.doc('messages/first').new({
  ok: true
});

// doc.isNew === true
// doc.isSaved === false

await doc.save();

// doc.isNew === false
// now doc is subscribed to onSnashot listener

existing(): Document

Creates Document instance which is automatically subscribed to onSnapshot listener.

let doc = store.doc('messages/first').existing();
// doc.isNew === false

async load({ optional: false }): Document or undefined

Loads document and creates Document instance for it.

let doc = await store.doc('messages/first').load({ optional: true });

If document doesn't exist and optional is:

  • true: undefined is returned
  • false: SwoofError with { code: 'document/missing' } is thrown

CollectionReference

id: string

Dollection id

path: string

Collection full path

doc(path): DocumentReference

Creates nested document reference

let ref = store.collection('messages').doc(); // generated id
let ref = store.collection('messages').doc('first');

conditions

There are also all firestore condition operators which all also return QueryableReference for further conditions and query(), load() methods.

  • where()
  • orderBy()
  • limit()
  • limitToLast()
  • startAt()
  • startAfter()
  • endAt()
  • endBefore()

query({ type: 'array' }): ArrayQuery or SingleQuery

Creates onSnapshot supporting Query instance. There are two types: array, single.

  • array query has content property which is array of Document instances
  • single query has content property which is Document instance or null
let array = store.collection('messages').query();
let single = store.collection('messages').orderBy('createdAt', 'asc').limit(1).query({ type: 'single' });

async load(): Array

Loads documents from firestore and creates Document instances for each of them.

let ref = store.collection('messages').load();
let array = await ref.lod(); // [ <Document>, ... ]

first({ optional: false }): Document or undefined

Loads first document from firestore and creates Document instance

let zeeba = await store.collection('messages').where('name', '==', 'zeeba').limit(1).first();

If document doesn't exist and optional is:

  • true: undefined is returned
  • false: SwoofError with { code: 'document/missing' } is thrown

Document extends Model

Document instance represents one firestore document.

let doc = store.doc('messages/first').new({
  ok: true
});

store: Store

Store for which this document is created.

ref: DocumentReference

DocumentReference for this document

id: string

Document id

path: string

Document full path

promise: Promise

Promise which is resolved after 1st load or 1st onSnapshot call

data: ObjectProxy

Document's data.

let doc = await store.doc('messages/first').load();
doc.data.name = 'new name';
// or
doc.data = { name: 'new name' };

Both editing properties directly or replacing data will trigger Svelte component renders.

merge(props): undefined

Deep merge document data

let doc = store.doc('messages/first').new({
  name: 'zeeba',
  thumbnail: {
    size: {
      width: 100,
      height: 100
    },
    url: null
  }
});

doc.merge({
  thumbnail: {
    url: 'https:/....'
  }
});

async load({ force: false }): Document

Loads document if it's not already loaded.

let doc = await store.doc('messages/first').existing();
await doc.load(); // loads
await doc.load(); // ignores. already loade
await doc.load({ force: true }); // loads or reloads

async reload(): Document

Reloads document. The same as doc.load({ force: true })

async save({ force: false, merge: false }): Document

Saves document if isDirty is true.

let doc = await store.doc('messages/first').new({
  ok: true
});

await doc.save(); // saves
await doc.save(); // ignores. not dirty
doc.data.name = 'zeeba';
await doc.save(); // saves
await doc.save({ force: true }); // saves even if not dirty
await doc.save({ merge: true }) // does `ref.set(data, { merge: true });

async delete(): Document

Deletes a document

let doc = await store.doc('messages/first');
await doc.delete();

serialized: Object

Returns JSON debugish representation of document.

let doc = await store.doc('messages/first').load();
{
  id: "first",
  path: "messages/first",
  exists: true,
  isNew: false,
  isDirty: false,
  isLoading: false,
  isSaving: false,
  isLoaded: true,
  isError: false,
  error: null,
  data: {
    name: "Zeeba"
  }
}

toJSON(): Object

Basically same as serialized with additional data

Query extends Model

onSnapshot aware query.

let array = store.collection('messages').where('status', '==', 'sent').query({ type: 'array' });
let single = store.collection('messages').limit(1).query({ type: 'single' });

promise: Promise

Promise which is resolved after 1st load or 1st onSnapshot call.

let query = store.collection('messages').query();
await query.promise; // resolves after 1st load or onSnapshot

async load({ force: false }): Query

Loads query if it is not already loaded. See Document.load for details on force.

let query = store.collection('messages').query();
await query.load();
// isLoaded === true
await query.load(); // doesn't do anything
await query.load({ force: true }); // loads

reload(): Query

Relaods query. Same as load({ force: true })

string: string

More or less readable query as a string.

serialized: object

Debugish query status representation

{
  error: null
  isError: false
  isLoaded: false
  isLoading: false
  string: "messages.where(status, ==, sent).limit(10)"
}

content

if { type } is:

  • array (default): array of Document instances
  • single: single (first) Document instance or null

Auth

let auth = store.auth;

Sign in

await auth.methods.anonymous.signIn();
await auth.methods.email.signIn(email, password);
await auth.methods.anonymous.signIn();
let user = auth.user;
await user.link('email', email, password);

User

let user = auth.user;
await user.delete();
await user.signOut();
import { User, toString } from 'swoof';

export default class DummyUser extends User {

  constructor(store, user) {
    super(store, user);
  }

  // restoe is called with user arg only if
  // user.uid === this.user.uid
  async restore(user) {
    if(user) {
      this.user = user;
    }
  }

  toString() {
    let { uid, email } = this;
    return toString(this, `${email || uid}`);
  }

}

Storage

let storage = store.storage;
let ref = storage.ref(`users/${uid}/avatar`);

let task = ref.put({
  type: 'data',
  data: file,
  metadata: {
    contentType: file.type
  }
});

await task.promise;
let ref = storage.ref(`users/${uid}/avatar`);
await ref.url();
await ref.metadata();
await ref.update({ contentType: 'image/png' });

Task extends Model

import { Model, writable, properties, objectToJSON } from 'swoof';

const {
  attr
} = properties;

class Storage extends Model {

  constructor() {
    super();
    this.property('task', attr(null))
  }

  async upload() {
    let task = store.storage.ref('hello').put({
      type: 'string',
      format: 'raw',
      data: 'hey there',
      metadata: {
        contentType: 'text/plain'
      }
    });
    this.task = task;
  }

  get serialized() {
    let { task } = this;
    return {
      task: objectToJSON(task)
    };
  }

}

let model = writable(new Storage());

Functions

await store.functions.call('hey-there', { ok: true });
await store.functions.region('us-central1').call('hey-there', { ok: true });

Issues

process is not defined

Uncaught ReferenceError: process is not defined

add plugin-replace to rollup config:

// rollup.config.js
import replace from '@rollup/plugin-replace';

plugins([
  //...
  svelte({
    // ...
  }),
  replace({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  }),
  // ...
])

'registerComponent' of undefined

Uncaught TypeError: Cannot read property 'registerComponent' of undefined

update plugin-commonjs:

// package.json
"devDependencies": {
    // ...
    "@rollup/plugin-commonjs": "^15.0.0"
}

TODO

  • alias() property
  • diff doc onSnapshot changes + state and do writable.set(this) only if there are changes present
  • models() property
  • tap: needs some kind of tool to forward change notifications to nested models
  • add basic auth support (sign up, sign in (email, anon), forgot password, link account, sign out)
  • add basic storage support

Top categories

Loading Svelte Themes