Swoof is Google Firebase Firestore, Auth, Storage, Functions library for Svelte.
Proof of concept
See /dummy
for some examples.
$ 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.
import { swoof } from 'swoof';
Creates FirebaseApp and links it to the name.
Creates and returns swoof store with given identifier and configuration name.
Returns existing store for identifier.
swoof.create('main', 'production'); // once
// somewhere else
let store = swoof.store('main');
Destroys internal FirebaseApp instances
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>
Creates Svelte writable for sfoof model instance or tree.
await load(....modelsOrPromises);
import { swoof } from 'swoof';
let store = swoof.store('main');
Creates swoof firestore document reference.
let ref = store.doc('messages/first');
Creates swoof firestore collection reference.
let ref = store.doc('messages/first/comments');
let doc = store.doc('messages/first').new({
text: 'hey there',
createdAt: store.serverTimestamp()
});
await doc.save();
let ref = store.doc('messages/first');
let ref = store.collection('messages').doc('first');
let ref = store.collection('messages').doc(); // generated id
Document id
Document path
Creates nested Collection Reference
let coll = store.doc('messages/first').collection('comments');
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
Creates Document instance which is automatically subscribed to onSnapshot listener.
let doc = store.doc('messages/first').existing();
// doc.isNew === false
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 returnedfalse
: SwoofError
with { code: 'document/missing' }
is thrownDollection id
Collection full path
Creates nested document reference
let ref = store.collection('messages').doc(); // generated id
let ref = store.collection('messages').doc('first');
There are also all firestore condition operators which all also return QueryableReference
for further conditions and query()
, load()
methods.
Creates onSnapshot
supporting Query instance. There are two types: array
, single
.
content
property which is array of Document instancescontent
property which is Document instance or nulllet array = store.collection('messages').query();
let single = store.collection('messages').orderBy('createdAt', 'asc').limit(1).query({ type: 'single' });
Loads documents from firestore and creates Document instances for each of them.
let ref = store.collection('messages').load();
let array = await ref.lod(); // [ <Document>, ... ]
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 returnedfalse
: SwoofError
with { code: 'document/missing' }
is thrownDocument instance represents one firestore document.
let doc = store.doc('messages/first').new({
ok: true
});
Store for which this document is created.
DocumentReference for this document
Document id
Document full path
Promise which is resolved after 1st load or 1st onSnapshot call
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.
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:/....'
}
});
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
Reloads document. The same as doc.load({ force: true })
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 });
Deletes a document
let doc = await store.doc('messages/first');
await doc.delete();
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"
}
}
Basically same as serialized with additional data
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 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
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
Relaods query. Same as load({ force: true })
More or less readable query as a string.
Debugish query status representation
{
error: null
isError: false
isLoaded: false
isLoading: false
string: "messages.where(status, ==, sent).limit(10)"
}
if { type }
is:
array
(default): array of Document instancessingle
: single (first) Document instance or nulllet auth = store.auth;
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);
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}`);
}
}
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' });
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());
await store.functions.call('hey-there', { ok: true });
await store.functions.region('us-central1').call('hey-there', { ok: true });
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)
}),
// ...
])
Uncaught TypeError: Cannot read property 'registerComponent' of undefined
update plugin-commonjs
:
// package.json
"devDependencies": {
// ...
"@rollup/plugin-commonjs": "^15.0.0"
}