{"id":15183832,"url":"https://github.com/ampatspell/swoof","last_synced_at":"2025-10-01T23:30:32.487Z","repository":{"id":65510441,"uuid":"305230770","full_name":"ampatspell/swoof","owner":"ampatspell","description":"🔥  Google Firebase Firestore, Auth, Storage, Functions library for Svelte.","archived":true,"fork":false,"pushed_at":"2020-10-24T17:09:18.000Z","size":1078,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-17T15:43:52.254Z","etag":null,"topics":["auth","firebase","firestore","functions","models","observale","onsnapshot","storage","svelte","svelte3","sveltejs","writable"],"latest_commit_sha":null,"homepage":"https://github.com/ampatspell/swoof","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ampatspell.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-10-19T01:20:08.000Z","updated_at":"2023-01-28T12:09:14.000Z","dependencies_parsed_at":"2023-01-26T18:01:40.001Z","dependency_job_id":null,"html_url":"https://github.com/ampatspell/swoof","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ampatspell%2Fswoof","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ampatspell%2Fswoof/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ampatspell%2Fswoof/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ampatspell%2Fswoof/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ampatspell","download_url":"https://codeload.github.com/ampatspell/swoof/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234909086,"owners_count":18905504,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth","firebase","firestore","functions","models","observale","onsnapshot","storage","svelte","svelte3","sveltejs","writable"],"created_at":"2024-09-27T17:01:36.812Z","updated_at":"2025-10-01T23:30:32.008Z","avatar_url":"https://github.com/ampatspell.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Swoof\n\nSwoof is Google Firebase Firestore, Auth, Storage, Functions library for Svelte.\n\n\u003e Proof of concept\n\nSee `/dummy` for some examples.\n\n\u003c!-- TOC depthFrom:2 --\u003e\n\n- [Setting up](#setting-up)\n- [API](#api)\n  - [swoof](#swoof)\n    - [configure(name, config): undefined](#configurename-config-undefined)\n    - [create(identifier, name): store](#createidentifier-name-store)\n    - [store(identifier): store or undefined](#storeidentifier-store-or-undefined)\n    - [destroy(): undefined](#destroy-undefined)\n  - [Model](#model)\n    - [writable(model): svelte/writable](#writablemodel-sveltewritable)\n    - [Properties](#properties)\n    - [load()](#load)\n  - [Store](#store)\n    - [doc(path): DocumentReference](#docpath-documentreference)\n    - [collection(path): CollectionReference](#collectionpath-collectionreference)\n    - [serverTimestamp(): firestore.FieldValue.ServerTimestamp](#servertimestamp-firestorefieldvalueservertimestamp)\n  - [DocumentReference](#documentreference)\n    - [id: string](#id-string)\n    - [path: string](#path-string)\n    - [collection(path): CollectionReference](#collectionpath-collectionreference-1)\n    - [new(props): Document](#newprops-document)\n    - [existing(): Document](#existing-document)\n    - [async load({ optional: false }): Document or undefined](#async-load-optional-false--document-or-undefined)\n  - [CollectionReference](#collectionreference)\n    - [id: string](#id-string-1)\n    - [path: string](#path-string-1)\n    - [doc(path): DocumentReference](#docpath-documentreference-1)\n    - [conditions](#conditions)\n    - [query({ type: 'array' }): ArrayQuery or SingleQuery](#query-type-array--arrayquery-or-singlequery)\n    - [async load(): Array\u003cDocument\u003e](#async-load-arraydocument)\n    - [first({ optional: false }): Document or undefined](#first-optional-false--document-or-undefined)\n  - [Document extends Model](#document-extends-model)\n    - [store: Store](#store-store)\n    - [ref: DocumentReference](#ref-documentreference)\n    - [id: string](#id-string-2)\n    - [path: string](#path-string-2)\n    - [promise: Promise\u003cDocument\u003e](#promise-promisedocument)\n    - [data: ObjectProxy](#data-objectproxy)\n    - [merge(props): undefined](#mergeprops-undefined)\n    - [async load({ force: false }): Document](#async-load-force-false--document)\n    - [async reload(): Document](#async-reload-document)\n    - [async save({ force: false, merge: false }): Document](#async-save-force-false-merge-false--document)\n    - [async delete(): Document](#async-delete-document)\n    - [serialized: Object](#serialized-object)\n    - [toJSON(): Object](#tojson-object)\n  - [Query extends Model](#query-extends-model)\n    - [promise: Promise\u003cQuery\u003e](#promise-promisequery)\n    - [async load({ force: false }): Query](#async-load-force-false--query)\n    - [reload(): Query](#reload-query)\n    - [string: string](#string-string)\n    - [serialized: object](#serialized-object)\n    - [content](#content)\n  - [Auth](#auth)\n    - [Sign in](#sign-in)\n    - [Link anonymous to credentials](#link-anonymous-to-credentials)\n    - [User](#user)\n  - [Storage](#storage)\n    - [Task extends Model](#task-extends-model)\n  - [Functions](#functions)\n- [Issues](#issues)\n  - [process is not defined](#process-is-not-defined)\n  - ['registerComponent' of undefined](#registercomponent-of-undefined)\n- [TODO](#todo)\n\n\u003c!-- /TOC --\u003e\n\n## Setting up\n\n```\n$ npm install swoof --save-dev\n```\n\n``` svelte\n// App.svete\n\u003cscript\u003e\n  import { swoof, state, setGlobal, User } from 'swoof';\n  import SomethingNice from './SomethingNice.svelte';\n\n  class FancyUser extends User {\n  }\n\n  let { firebase } = process.env.CONFIG;\n\n  let config = {\n    firebase: {\n      apiKey: \"...\",\n      authDomain: \"...\",\n      databaseURL: \"...\",\n      projectId: \"...\",\n      storageBucket: \"...\",\n      messagingSenderId: \"...\",\n      appId: \"...\"\n    },\n    firestore: {\n      enablePersistence: true\n    },\n    swoof: {\n      auth: {\n        User: FancyUser\n      },\n      // override default region for store.functions\n      // functions: {\n      //   region: 'us-central1'\n      // }\n    }\n  };\n\n  // internally creates FirebaseApp named main\n  swoof.configure('main', config);\n\n  // creates store named `main` using firebase app named `main`\n  // swoof supports multiple firebase apps\n  let store = swoof.create('main', 'main');\n\n  // Optional tools for playing around in console\n  setGlobal({ store });\n  setGlobal({ state });\n\u003c/script\u003e\n\n\u003cSomethingNice/\u003e\n\n\u003cstyle\u003e\n\u003c/style\u003e\n```\n\n``` javascript\n// console\nawait store.doc('message/hello').new({ text: 'hey there' }).save();\n```\n\nIf you're getting weird build or runtime errors, see below.\n\n## API\n\n### swoof\n\n``` javascript\nimport { swoof } from 'swoof';\n```\n\n#### configure(name, config): undefined\n\nCreates FirebaseApp and links it to the name.\n\n#### create(identifier, name): store\n\nCreates and returns swoof store with given identifier and configuration name.\n\n#### store(identifier): store or undefined\n\nReturns existing store for identifier.\n\n``` javascript\nswoof.create('main', 'production'); // once\n\n// somewhere else\nlet store = swoof.store('main');\n```\n\n#### destroy(): undefined\n\nDestroys internal FirebaseApp instances\n\n### Model\n\n\u003e Soon. See /dummy for examples\n\n``` javascript\n// lib/messages.js\nimport { Model, properties } from 'swoof';\n\nconst {\n  attr,\n  models,\n  tap\n} = properties;\n\nclass Message extends Model {\n\n  constructor(message) {\n    super();\n    // tap doesn't bind, just forwards change notifications in this context\n    this.property('doc', tap(doc));\n  }\n\n  get data() {\n    return this.doc.data;\n  }\n\n  get text() {\n    return this.data.text;\n  }\n\n  async save() {\n    await this.doc.save();\n  }\n\n}\n\nexport default class Messages extends Model {\n\n  constructor(store) {\n    super();\n    this.store = this;\n    this.coll = store.collection('messages');\n\n    // query autosubscribes to ref.onSnapshot\n    this.property('query', attr(this.coll.orderBy('createdAt').query()));\n\n    // Message models are automatically created for each document.\n    // then added/removed based on snapshot.docChanges\n    this.property('messages', models('query.content', doc =\u003e new Message(doc)));\n  }\n\n  async add(text) {\n    let { store } = this;\n    let doc = this.coll.doc().new({\n      text,\n      createdAt: store.serverTimestamp();\n    });\n    await doc.save();\n  }\n\n}\n```\n\n``` svelte\n\u003cscript\u003e\n  import { store } from 'swoof';\n  import Messages from './lib/messages';\n\n  // Writable when subscribed starts all onSnapshot listeners and\n  // property change notifications\n  // Everything is torn down when last subscriber unsubscribes.\n  let messages = writable(new Messages(store));\n\u003c/script\u003e\n\n\u003c!-- use \"$\" only for `messages` - first level --\u003e\n\n\u003cdiv\u003e{$messages.count} messages.\u003c/div\u003e\n\u003cdiv\u003e\n  {#each $messages.message as message}\n    \u003cdiv\u003e{message.text}\u003c/div\u003e\n  {/each}\n\u003c/div\u003e\n```\n\n#### writable(model): svelte/writable\n\nCreates Svelte writable for sfoof model instance or tree.\n\n#### Properties\n\n* attr\n* array\n* models\n* tap\n* alias\n* logger\n\n#### load()\n\n``` javascript\nawait load(....modelsOrPromises);\n```\n\n### Store\n\n``` javascript\nimport { swoof } from 'swoof';\nlet store = swoof.store('main');\n```\n\n#### doc(path): DocumentReference\n\nCreates swoof firestore document reference.\n\n``` javascript\nlet ref = store.doc('messages/first');\n```\n\n#### collection(path): CollectionReference\n\nCreates swoof firestore collection reference.\n\n``` javascript\nlet ref = store.doc('messages/first/comments');\n```\n\n#### serverTimestamp(): firestore.FieldValue.ServerTimestamp\n\n``` javascript\nlet doc = store.doc('messages/first').new({\n  text: 'hey there',\n  createdAt: store.serverTimestamp()\n});\nawait doc.save();\n```\n\n### DocumentReference\n\n``` javascript\nlet ref = store.doc('messages/first');\nlet ref = store.collection('messages').doc('first');\nlet ref = store.collection('messages').doc(); // generated id\n```\n\n#### id: string\n\nDocument id\n\n#### path: string\n\nDocument path\n\n#### collection(path): CollectionReference\n\nCreates nested Collection Reference\n\n``` javascript\nlet coll = store.doc('messages/first').collection('comments');\n```\n\n#### new(props): Document\n\nCreates Document instance which is not automatically subscribed to onSnapshot listener.\n\nSubscription to onSnapshot happens right after `save` or `load`.\n\n``` javascript\nlet doc = store.doc('messages/first').new({\n  ok: true\n});\n\n// doc.isNew === true\n// doc.isSaved === false\n\nawait doc.save();\n\n// doc.isNew === false\n// now doc is subscribed to onSnashot listener\n```\n\n#### existing(): Document\n\nCreates Document instance which is automatically subscribed to onSnapshot listener.\n\n``` javascript\nlet doc = store.doc('messages/first').existing();\n// doc.isNew === false\n```\n\n#### async load({ optional: false }): Document or undefined\n\nLoads document and creates Document instance for it.\n\n``` javascript\nlet doc = await store.doc('messages/first').load({ optional: true });\n```\n\nIf document doesn't exist and optional is:\n* `true`: `undefined` is returned\n* `false`: `SwoofError` with `{ code: 'document/missing' }` is thrown\n\n### CollectionReference\n\n#### id: string\n\nDollection id\n\n#### path: string\n\nCollection full path\n\n#### doc(path): DocumentReference\n\nCreates nested document reference\n\n``` javascript\nlet ref = store.collection('messages').doc(); // generated id\nlet ref = store.collection('messages').doc('first');\n```\n\n#### conditions\n\nThere are also all firestore condition operators which all also return `QueryableReference` for further conditions and `query()`, `load()` methods.\n\n* where()\n* orderBy()\n* limit()\n* limitToLast()\n* startAt()\n* startAfter()\n* endAt()\n* endBefore()\n\n#### query({ type: 'array' }): ArrayQuery or SingleQuery\n\nCreates `onSnapshot` supporting Query instance. There are two types: `array`, `single`.\n\n* array query has `content` property which is array of Document instances\n* single query has `content` property which is Document instance or null\n\n``` javascript\nlet array = store.collection('messages').query();\nlet single = store.collection('messages').orderBy('createdAt', 'asc').limit(1).query({ type: 'single' });\n```\n\n#### async load(): Array\u003cDocument\u003e\n\nLoads documents from firestore and creates Document instances for each of them.\n\n``` javascript\nlet ref = store.collection('messages').load();\nlet array = await ref.lod(); // [ \u003cDocument\u003e, ... ]\n```\n\n#### first({ optional: false }): Document or undefined\n\nLoads first document from firestore and creates Document instance\n\n``` javascript\nlet zeeba = await store.collection('messages').where('name', '==', 'zeeba').limit(1).first();\n```\n\nIf document doesn't exist and optional is:\n* `true`: `undefined` is returned\n* `false`: `SwoofError` with `{ code: 'document/missing' }` is thrown\n\n### Document extends Model\n\nDocument instance represents one firestore document.\n\n``` javascript\nlet doc = store.doc('messages/first').new({\n  ok: true\n});\n```\n\n#### store: Store\n\nStore for which this document is created.\n\n#### ref: DocumentReference\n\nDocumentReference for this document\n\n#### id: string\n\nDocument id\n\n#### path: string\n\nDocument full path\n\n#### promise: Promise\u003cDocument\u003e\n\nPromise which is resolved after 1st load or 1st onSnapshot call\n\n#### data: ObjectProxy\n\nDocument's data.\n\n``` javascript\nlet doc = await store.doc('messages/first').load();\ndoc.data.name = 'new name';\n// or\ndoc.data = { name: 'new name' };\n```\n\nBoth editing properties directly or replacing data will trigger Svelte component renders.\n\n#### merge(props): undefined\n\nDeep merge document data\n\n``` javascript\nlet doc = store.doc('messages/first').new({\n  name: 'zeeba',\n  thumbnail: {\n    size: {\n      width: 100,\n      height: 100\n    },\n    url: null\n  }\n});\n\ndoc.merge({\n  thumbnail: {\n    url: 'https:/....'\n  }\n});\n```\n\n#### async load({ force: false }): Document\n\nLoads document if it's not already loaded.\n\n``` javascript\nlet doc = await store.doc('messages/first').existing();\nawait doc.load(); // loads\nawait doc.load(); // ignores. already loade\nawait doc.load({ force: true }); // loads or reloads\n```\n\n#### async reload(): Document\n\nReloads document. The same as `doc.load({ force: true })`\n\n#### async save({ force: false, merge: false }): Document\n\nSaves document if `isDirty` is `true`.\n\n``` javascript\nlet doc = await store.doc('messages/first').new({\n  ok: true\n});\n\nawait doc.save(); // saves\nawait doc.save(); // ignores. not dirty\ndoc.data.name = 'zeeba';\nawait doc.save(); // saves\nawait doc.save({ force: true }); // saves even if not dirty\nawait doc.save({ merge: true }) // does `ref.set(data, { merge: true });\n```\n\n#### async delete(): Document\n\nDeletes a document\n\n``` javascript\nlet doc = await store.doc('messages/first');\nawait doc.delete();\n```\n\n#### serialized: Object\n\nReturns JSON debugish representation of document.\n\n``` javascript\nlet doc = await store.doc('messages/first').load();\n```\n\n``` javascript\n{\n  id: \"first\",\n  path: \"messages/first\",\n  exists: true,\n  isNew: false,\n  isDirty: false,\n  isLoading: false,\n  isSaving: false,\n  isLoaded: true,\n  isError: false,\n  error: null,\n  data: {\n    name: \"Zeeba\"\n  }\n}\n```\n\n#### toJSON(): Object\n\nBasically same as serialized with additional data\n\n### Query extends Model\n\nonSnapshot aware query.\n\n``` javascript\nlet array = store.collection('messages').where('status', '==', 'sent').query({ type: 'array' });\nlet single = store.collection('messages').limit(1).query({ type: 'single' });\n```\n\n#### promise: Promise\u003cQuery\u003e\n\nPromise which is resolved after 1st load or 1st onSnapshot call.\n\n``` javascript\nlet query = store.collection('messages').query();\nawait query.promise; // resolves after 1st load or onSnapshot\n```\n\n#### async load({ force: false }): Query\n\nLoads query if it is not already loaded. See `Document.load` for details on `force`.\n\n``` javascript\nlet query = store.collection('messages').query();\nawait query.load();\n// isLoaded === true\nawait query.load(); // doesn't do anything\nawait query.load({ force: true }); // loads\n```\n\n#### reload(): Query\n\nRelaods query. Same as `load({ force: true })`\n\n#### string: string\n\nMore or less readable query as a string.\n\n#### serialized: object\n\nDebugish query status representation\n\n``` javascript\n{\n  error: null\n  isError: false\n  isLoaded: false\n  isLoading: false\n  string: \"messages.where(status, ==, sent).limit(10)\"\n}\n```\n\n#### content\n\nif `{ type }` is:\n\n* `array` (default): array of Document instances\n* `single`: single (first) Document instance or null\n\n### Auth\n\n``` javascript\nlet auth = store.auth;\n```\n\n#### Sign in\n\n``` javascript\nawait auth.methods.anonymous.signIn();\nawait auth.methods.email.signIn(email, password);\n```\n\n#### Link anonymous to credentials\n\n``` javascript\nawait auth.methods.anonymous.signIn();\nlet user = auth.user;\nawait user.link('email', email, password);\n```\n\n#### User\n\n``` javascript\nlet user = auth.user;\nawait user.delete();\nawait user.signOut();\n```\n\n``` javascript\nimport { User, toString } from 'swoof';\n\nexport default class DummyUser extends User {\n\n  constructor(store, user) {\n    super(store, user);\n  }\n\n  // restoe is called with user arg only if\n  // user.uid === this.user.uid\n  async restore(user) {\n    if(user) {\n      this.user = user;\n    }\n  }\n\n  toString() {\n    let { uid, email } = this;\n    return toString(this, `${email || uid}`);\n  }\n\n}\n```\n\n### Storage\n\n``` javascript\nlet storage = store.storage;\n```\n\n``` javascript\nlet ref = storage.ref(`users/${uid}/avatar`);\n\nlet task = ref.put({\n  type: 'data',\n  data: file,\n  metadata: {\n    contentType: file.type\n  }\n});\n\nawait task.promise;\n```\n\n``` javascript\nlet ref = storage.ref(`users/${uid}/avatar`);\nawait ref.url();\nawait ref.metadata();\nawait ref.update({ contentType: 'image/png' });\n```\n\n#### Task extends Model\n\n``` javascript\nimport { Model, writable, properties, objectToJSON } from 'swoof';\n\nconst {\n  attr\n} = properties;\n\nclass Storage extends Model {\n\n  constructor() {\n    super();\n    this.property('task', attr(null))\n  }\n\n  async upload() {\n    let task = store.storage.ref('hello').put({\n      type: 'string',\n      format: 'raw',\n      data: 'hey there',\n      metadata: {\n        contentType: 'text/plain'\n      }\n    });\n    this.task = task;\n  }\n\n  get serialized() {\n    let { task } = this;\n    return {\n      task: objectToJSON(task)\n    };\n  }\n\n}\n\nlet model = writable(new Storage());\n```\n\n### Functions\n\n``` javascript\nawait store.functions.call('hey-there', { ok: true });\nawait store.functions.region('us-central1').call('hey-there', { ok: true });\n```\n\n## Issues\n\n### process is not defined\n\n```\nUncaught ReferenceError: process is not defined\n```\n\nadd `plugin-replace` to rollup config:\n\n``` javascript\n// rollup.config.js\nimport replace from '@rollup/plugin-replace';\n\nplugins([\n  //...\n  svelte({\n    // ...\n  }),\n  replace({\n    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)\n  }),\n  // ...\n])\n```\n\n### 'registerComponent' of undefined\n\n```\nUncaught TypeError: Cannot read property 'registerComponent' of undefined\n```\n\nupdate `plugin-commonjs`:\n\n``` javascript\n// package.json\n\"devDependencies\": {\n    // ...\n    \"@rollup/plugin-commonjs\": \"^15.0.0\"\n}\n```\n\n## TODO\n\n- [ ] alias() property\n- [ ] diff doc onSnapshot changes + state and do writable.set(this) only if there are changes present\n- [x] models() property\n- [x] tap: needs some kind of tool to forward change notifications to nested models\n- [x] add basic auth support (sign up, sign in (email, anon), forgot password, link account, sign out)\n- [x] add basic storage support\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fampatspell%2Fswoof","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fampatspell%2Fswoof","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fampatspell%2Fswoof/lists"}