diff --git a/README.md b/README.md
index a63adc1..c383e49 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,9 @@ Hello {$user?.uid}
3. Listen to realtime data.
-Use the `$` as much as you want - it will only result in one Firebase read request. When all the subscriptions are removed, it will automatically unsubscribe.
+Use the `$` as much as you want - it will only result in one Firebase read request. When all the subscriptions are removed, it will automatically unsubscribe. To get the data from stores, use the `data` property, if an
+error occurs, the error property will be set with an Error instance.
+
```svelte
-{$post?.content}
-{$post?.title}
+{$post?.data?.content}
+{$post?.data?.title}
+{$post?.error?.message}
```
Or better yet, use the built in `Doc` and `Collection` components for Firestore, or `Node` and `NodeList` components for Realtime Database. See below.
@@ -135,9 +138,9 @@ Subscribe to realtime data. The store will unsubscribe automatically to avoid un
const posts = collectionStore(firestore, 'posts');
-{$post?.content}
+{$post?.data?.content}
-{#each $posts as post}
+{#each $posts?.data as post}
{/each}
```
@@ -301,16 +304,19 @@ Pass a `startWith` value to bypass the loading state. This is useful in SvelteKi
### Collection
-Collections provides array of objects containing the document data, as well as the `id` and `ref` for each result. It also provides a `count` slot prop for number of docs in the query.
+Collections provides array of objects containing the document data, as well as the `id` and `ref` for each result. It also provides a `count` slot prop for number of docs in the query. Errors can be handled with the `error` slot prop.
```svelte
-
+
Fetched {count} documents
{#each data as post}
{post.id}
{post.ref.path}
{post.content}
{/each}
+ {#if error}
+ Error: {error.message}
+ {/if}
```
@@ -376,49 +382,55 @@ Fetch lists of nodes from the Realtime Database and listen to their data in real
### DownloadURL
-DownloadURL provides a `link` to download a file from Firebase Storage and its `reference`.
+DownloadURL provides a `link` to download a file from Firebase Storage and its `reference`. Errors can be handled with the `error` slot prop.
```svelte
-
- Download {ref?.name}
+
+ Download {ref?.name}
+ {#if error}
+ Error: {error.message}
+ {/if}
```
### StorageList
-StorageList provides a list of `items` and `prefixes` corresponding to the list of objects and sub-folders at a given Firebase Storage path.
+StorageList provides a list of `items` and `prefixes` corresponding to the list of objects and sub-folders at a given Firebase Storage path. Errors can be handled with the `error` slot prop.
```svelte
-
-
- {#if list === null}
- Loading...
- {:else if list.prefixes.length === 0 && list.items.length === 0}
- Empty
- {:else}
-
- {#each list.prefixes as prefix}
-
- {prefix.name}
-
- {/each}
-
- {#each list.items as item}
-
- {item.name}
-
- {/each}
+
+
+ {#if list === null}
+ Loading...
+ {:else if list.prefixes.length === 0 && list.items.length === 0}
+ Empty
+ {:else}
+
+ {#each list.prefixes as prefix}
+
+ {prefix.name}
+
+ {/each}
+
+ {#each list.items as item}
+
+ {item.name}
+
+ {/each}
+ {/if}
+
+ {#if error}
+ Error: {error.message}
{/if}
-
```
### UploadTask
-Upload a file with progress tracking
+Upload a file with progress tracking. If error occurs, the `error` slot prop will be defined. The `snapshot` slot prop provides access to the upload task's `state`, `ref`, and `progress` percentage.
```svelte
-
+
{#if snapshot?.state === "running"}
{progress}% uploaded
{/if}
@@ -428,6 +440,10 @@ Upload a file with progress tracking
Download
{/if}
+
+ {#if error}
+ Error: {error.message}
+ {/if}
```
diff --git a/package-lock.json b/package-lock.json
index e010b8b..4aab479 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "sveltefire",
- "version": "0.4.1",
+ "version": "0.4.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sveltefire",
- "version": "0.4.1",
+ "version": "0.4.3",
"devDependencies": {
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^2.0.0",
diff --git a/src/lib/components/Collection.svelte b/src/lib/components/Collection.svelte
index 14b373f..3258bba 100644
--- a/src/lib/components/Collection.svelte
+++ b/src/lib/components/Collection.svelte
@@ -18,6 +18,7 @@
interface $$Slots {
default: {
data: Data[];
+ error: Error | null;
ref: CollectionReference | Query | null;
count: number;
firestore?: Firestore;
@@ -27,7 +28,7 @@
{#if $store !== undefined}
-
+
{:else}
{/if}
diff --git a/src/lib/components/Doc.svelte b/src/lib/components/Doc.svelte
index 4feac31..15d9c54 100644
--- a/src/lib/components/Doc.svelte
+++ b/src/lib/components/Doc.svelte
@@ -16,7 +16,8 @@
interface $$Slots {
default: {
- data: Data;
+ data: Data | null | undefined;
+ error: Error | null;
ref: DocumentReference | null;
firestore?: Firestore;
};
@@ -25,7 +26,7 @@
{#if $store !== undefined && $store !== null}
-
+
{:else}
{/if}
diff --git a/src/lib/components/DownloadURL.svelte b/src/lib/components/DownloadURL.svelte
index f2349f9..1070f8b 100644
--- a/src/lib/components/DownloadURL.svelte
+++ b/src/lib/components/DownloadURL.svelte
@@ -9,13 +9,13 @@
const store = downloadUrlStore(storage!, ref);
interface $$Slots {
- default: { link: string | null; ref: StorageReference | null; storage?: FirebaseStorage },
+ default: { link: string | null; error: Error | null; ref: StorageReference | null; storage?: FirebaseStorage },
loading: {},
}
-{#if $store !== undefined}
-
+{#if $store !== undefined && $store !== null}
+
{:else}
{/if}
diff --git a/src/lib/components/StorageList.svelte b/src/lib/components/StorageList.svelte
index 5b1966f..73105a2 100644
--- a/src/lib/components/StorageList.svelte
+++ b/src/lib/components/StorageList.svelte
@@ -9,13 +9,13 @@
const listStore = storageListStore(storage!, ref);
interface $$Slots {
- default: { list: ListResult | null; ref: StorageReference | null; storage?: FirebaseStorage },
+ default: { list: ListResult | null; ref: StorageReference | null; storage?: FirebaseStorage, error: Error | null },
loading: {},
}
-{#if $listStore !== undefined}
-
+{#if $listStore !== undefined && $listStore !== null}
+
{:else}
{/if}
diff --git a/src/lib/components/UploadTask.svelte b/src/lib/components/UploadTask.svelte
index 0f7dbed..6009de1 100644
--- a/src/lib/components/UploadTask.svelte
+++ b/src/lib/components/UploadTask.svelte
@@ -19,6 +19,7 @@
interface $$Slots {
default: {
task: UploadTask | undefined;
+ error: Error | null;
ref: StorageReference | null;
snapshot: UploadTaskSnapshot | null;
progress: number;
@@ -26,9 +27,9 @@
};
}
- $: progress = ($upload?.bytesTransferred! / $upload?.totalBytes!) * 100 ?? 0;
+ $: progress = ($upload?.data?.bytesTransferred! / $upload?.data?.totalBytes!) * 100 ?? 0;
-{#if $upload !== undefined}
-
+{#if $upload !== undefined && $upload !== null}
+
{/if}
diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts
index 11b7a7b..0043725 100644
--- a/src/lib/stores/auth.ts
+++ b/src/lib/stores/auth.ts
@@ -1,5 +1,4 @@
import { writable } from "svelte/store";
-import { getFirebaseContext } from "./sdk.js";
import { onAuthStateChanged, type Auth } from "firebase/auth";
/**
diff --git a/src/lib/stores/firestore.ts b/src/lib/stores/firestore.ts
index 6bdd771..d369146 100644
--- a/src/lib/stores/firestore.ts
+++ b/src/lib/stores/firestore.ts
@@ -1,14 +1,21 @@
-import { writable } from "svelte/store";
+import { readable } from "svelte/store";
import { doc, collection, onSnapshot } from "firebase/firestore";
import type {
Query,
CollectionReference,
DocumentReference,
Firestore,
+ DocumentData,
} from "firebase/firestore";
+import type { FirebaseError } from "firebase/app";
+
+type DocValue = {
+ data: T | null | undefined;
+ error: Error | null;
+};
interface DocStore {
- subscribe: (cb: (value: T | null) => void) => void | (() => void);
+ subscribe: (cb: (value: DocValue) => void) => void | (() => void);
ref: DocumentReference | null;
id: string;
}
@@ -28,7 +35,7 @@ export function docStore(
// Fallback for SSR
if (!globalThis.window) {
- const { subscribe } = writable(startWith);
+ const { subscribe } = readable({data: startWith, error: null});
return {
subscribe,
ref: null,
@@ -38,10 +45,7 @@ export function docStore(
// Fallback for missing SDK
if (!firestore) {
- console.warn(
- "Firestore is not initialized. Are you missing FirebaseApp as a parent component?"
- );
- const { subscribe } = writable(null);
+ const { subscribe } = readable({data: null, error: new Error("Firestore is not initialized. Are you missing FirebaseApp as a parent component?")});
return {
subscribe,
ref: null,
@@ -49,16 +53,25 @@ export function docStore(
};
}
- const docRef =
- typeof ref === "string"
- ? (doc(firestore, ref) as DocumentReference)
- : ref;
+ let docRef: DocumentReference;
+
+ try {
+ docRef = typeof ref === "string" ? (doc(firestore, ref) as DocumentReference) : ref;
+ } catch (error) {
+ const { subscribe } = readable({data: null, error: new Error(`Failed to create DocumentReference from path "${ref}" : ${error}`)});
+ return {
+ subscribe,
+ ref: null,
+ id: "",
+ };
+ }
- const { subscribe } = writable(startWith, (set) => {
+ const { subscribe } = readable>({data: startWith, error: null}, (set) => {
unsubscribe = onSnapshot(docRef, (snapshot) => {
- set((snapshot.data() as T) ?? null);
+ set({data: (snapshot.data() as T) ?? null, error: null});
+ }, (error: FirebaseError) => {
+ set({data: null, error});
});
-
return () => unsubscribe();
});
@@ -69,8 +82,13 @@ export function docStore(
};
}
+type CollectionValue = {
+ data: T | [];
+ error: Error | null;
+};
+
interface CollectionStore {
- subscribe: (cb: (value: T | []) => void) => void | (() => void);
+ subscribe: (cb: (value: CollectionValue) => void) => void | (() => void);
ref: CollectionReference | Query | null;
}
@@ -89,7 +107,7 @@ export function collectionStore(
// Fallback for SSR
if (!globalThis.window) {
- const { subscribe } = writable(startWith);
+ const { subscribe } = readable({data: startWith, error: null});
return {
subscribe,
ref: null,
@@ -101,21 +119,34 @@ export function collectionStore(
console.warn(
"Firestore is not initialized. Are you missing FirebaseApp as a parent component?"
);
- const { subscribe } = writable([]);
+ const { subscribe } = readable({data: [], error: new Error("Firestore is not initialized. Are you missing FirebaseApp as a parent component?")});
return {
subscribe,
ref: null,
};
}
- const colRef = typeof ref === "string" ? collection(firestore, ref) : ref;
+ let colRef: CollectionReference | Query;
+
+ try {
+ colRef = typeof ref === "string" ? collection(firestore, ref) : ref;
+ } catch (error) {
+ const { subscribe } = readable({data: [], error: new Error(`Failed to create DocumentReference from path "${ref}" : ${error}`)});
+ return {
+ subscribe,
+ ref: null,
+ };
+ }
- const { subscribe } = writable(startWith, (set) => {
- unsubscribe = onSnapshot(colRef, (snapshot) => {
- const data = snapshot.docs.map((s) => {
+ const { subscribe } = readable>({data: startWith, error: null}, (set) => {
+ unsubscribe = onSnapshot(colRef, (snapshot): void => {
+ const data = snapshot.docs.map((s: DocumentData) => {
return { id: s.id, ref: s.ref, ...s.data() } as T;
});
- set(data);
+ set({data, error: null});
+ },
+ (error: FirebaseError) => {
+ set({data: [], error});
});
return () => unsubscribe();
diff --git a/src/lib/stores/storage.ts b/src/lib/stores/storage.ts
index 168207a..8958f53 100644
--- a/src/lib/stores/storage.ts
+++ b/src/lib/stores/storage.ts
@@ -13,14 +13,20 @@ import type {
UploadTaskSnapshot,
UploadMetadata,
} from "firebase/storage";
+import { error } from "@sveltejs/kit";
const defaultListResult: ListResult = {
prefixes: [],
items: [],
};
+type StorageListValue = {
+ data: ListResult | null | undefined;
+ error: Error | null;
+};
+
interface StorageListStore {
- subscribe: (cb: (value: ListResult) => void) => void | (() => void);
+ subscribe: (cb: (value: StorageListValue) => void) => void | (() => void);
reference: StorageReference | null;
}
@@ -37,7 +43,7 @@ export function storageListStore(
): StorageListStore {
// Fallback for SSR
if (!globalThis.window) {
- const { subscribe } = readable(startWith);
+ const { subscribe } = readable({ data: startWith, error: null });
return {
subscribe,
reference: null,
@@ -46,22 +52,34 @@ export function storageListStore(
// Fallback for missing SDK
if (!storage) {
- console.warn(
- "Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?"
- );
- const { subscribe } = readable(defaultListResult);
+ console.error("Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?");
+ const { subscribe } = readable({data: null, error: new Error("Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?")});
return {
subscribe,
reference: null,
};
}
- const storageRef =
- typeof reference === "string" ? ref(storage, reference) : reference;
+ let storageRef: StorageReference;
+
+ try{
+ storageRef = typeof reference === "string" ? ref(storage, reference) : reference;
+ }
+ catch(error) {
+ console.error(`Failed to create StorageReference from path "${reference}" : ${error}`);
+ const { subscribe } = readable({data: null, error: new Error(`Failed to create StorageReference from path "${reference}" : ${error}`)});
+ return {
+ subscribe,
+ reference: null,
+ };
+ }
- const { subscribe } = readable(startWith, (set) => {
+ const { subscribe } = readable({data: startWith, error: null}, (set) => {
list(storageRef).then((snapshot) => {
- set(snapshot);
+ set({data: snapshot, error: null});
+ },
+ (error) => {
+ set({data: null, error});
});
});
@@ -71,8 +89,12 @@ export function storageListStore(
};
}
+type DownloadUrlValue = {
+ data: string | null;
+ error: Error | null;
+}
interface DownloadUrlStore {
- subscribe: (cb: (value: string | null) => void) => void | (() => void);
+ subscribe: (cb: (value: DownloadUrlValue) => void) => void | (() => void);
reference: StorageReference | null;
}
@@ -89,7 +111,7 @@ export function downloadUrlStore(
): DownloadUrlStore {
// Fallback for SSR
if (!globalThis.window) {
- const { subscribe } = readable(startWith);
+ const { subscribe } = readable({data: startWith, error: null});
return {
subscribe,
reference: null,
@@ -98,22 +120,36 @@ export function downloadUrlStore(
// Fallback for missing SDK
if (!storage) {
- console.warn(
+ console.error(
"Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?"
);
- const { subscribe } = readable(null);
+ const { subscribe } = readable({data: null, error: new Error("Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?")});
return {
subscribe,
reference: null,
};
}
- const storageRef =
- typeof reference === "string" ? ref(storage, reference) : reference;
+ let storageRef: StorageReference;
+
+ try{
+ storageRef = typeof reference === "string" ? ref(storage, reference) : reference;
+ }
+ catch(error) {
+ console.error(`Failed to create StorageReference from path "${reference}" : ${error}`);
+ const { subscribe } = readable({data: null, error: new Error(`Failed to create StorageReference from path "${reference}" : ${error}`)});
+ return {
+ subscribe,
+ reference: null,
+ };
+ }
- const { subscribe } = readable(startWith, (set) => {
+ const { subscribe } = readable({data: startWith, error: null}, (set) => {
getDownloadURL(storageRef).then((snapshot) => {
- set(snapshot);
+ set({data: snapshot, error: null});
+ },
+ (error) => {
+ set({data: null, error});
});
});
@@ -123,9 +159,14 @@ export function downloadUrlStore(
};
}
+type UploadTaskValue = {
+ data: UploadTaskSnapshot | null;
+ error: Error | null;
+}
+
interface UploadTaskStore {
subscribe: (
- cb: (value: UploadTaskSnapshot | null) => void
+ cb: (value: UploadTaskValue) => void
) => void | (() => void);
reference: StorageReference | null;
}
@@ -138,7 +179,7 @@ export function uploadTaskStore(
): UploadTaskStore {
// Fallback for SSR
if (!globalThis.window) {
- const { subscribe } = readable(null);
+ const { subscribe } = readable({data: null, error: null});
return {
subscribe,
reference: null,
@@ -150,31 +191,42 @@ export function uploadTaskStore(
console.warn(
"Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?"
);
- const { subscribe } = readable(null);
+ const { subscribe } = readable({data: null, error: new Error("Cloud Storage is not initialized. Are you missing FirebaseApp as a parent component?")});
return {
subscribe,
reference: null,
};
}
- const storageRef =
- typeof reference === "string" ? ref(storage, reference) : reference;
+ let storageRef: StorageReference;
+
+ try{
+ storageRef = typeof reference === "string" ? ref(storage, reference) : reference;
+ }
+ catch(error) {
+ console.error(`Failed to create StorageReference from path "${reference}" : ${error}`);
+ const { subscribe } = readable({data: null, error: new Error(`Failed to create StorageReference from path "${reference}" : ${error}`)});
+ return {
+ subscribe,
+ reference: null,
+ };
+ }
let unsubscribe: () => void;
- const { subscribe } = readable(null, (set) => {
+ const { subscribe } = readable({data: null, error: null}, (set) => {
const task = uploadBytesResumable(storageRef, data, metadata);
unsubscribe = task.on(
"state_changed",
(snapshot) => {
- set(snapshot);
+ set({data: snapshot, error: null});
},
(error) => {
console.error(error);
- set(task.snapshot);
+ set({data: task.snapshot, error});
},
() => {
- set(task.snapshot);
+ set({data: task.snapshot, error: null});
}
);
return () => unsubscribe();
diff --git a/src/routes/firestore-test/+page.svelte b/src/routes/firestore-test/+page.svelte
index 40d5894..7180459 100644
--- a/src/routes/firestore-test/+page.svelte
+++ b/src/routes/firestore-test/+page.svelte
@@ -69,4 +69,16 @@
addPost(user.uid)}>Add Post
+
+
Should show an error
+
+
+ {#if error !== null}
+ Error occured {error}
+ {:else}
+ No error while retrieving data
+ {/if}
+
diff --git a/src/routes/storage-test/+page.svelte b/src/routes/storage-test/+page.svelte
index 13c251d..4dd31d8 100644
--- a/src/routes/storage-test/+page.svelte
+++ b/src/routes/storage-test/+page.svelte
@@ -16,6 +16,8 @@
Storage Test
+Storage List
+
{#if list === null}
@@ -34,6 +36,12 @@
+Error handling (trying to fetch an invalid ref)
+
+
+ {error?.message}
+
+
Upload Task
diff --git a/tests/firestore.test.ts b/tests/firestore.test.ts
index 9442d69..7f80734 100644
--- a/tests/firestore.test.ts
+++ b/tests/firestore.test.ts
@@ -1,21 +1,32 @@
-import { expect, test } from '@playwright/test';
+import { expect, test, type Page } from '@playwright/test';
-test('Renders a single document', async ({ page }) => {
- await page.goto('/firestore-test');
- await expect(page.getByTestId('doc-data')).toContainText('Hi Mom');
-});
+test.describe.serial("Firestore", () => {
+ let page: Page;
+ test.beforeAll(async ({ browser }) => {
+ page = await browser.newPage();
+ await page.goto("/firestore-test");
+ });
-test('Renders a collection of items for an authenticated user in realtime', async ({ page }) => {
- await page.goto('/firestore-test');
- await page.getByRole('button', { 'name': 'Sign In'}).click({delay: 1000});
- await expect(page.getByTestId('count')).toContainText('0 posts');
- await page.getByRole('button', { name: 'Add Post' }).click();
- await expect(page.getByTestId('count')).toContainText('1 posts');
- await page.getByRole('button', { name: 'Add Post' }).click();
- await expect(page.getByTestId('count')).toContainText('2 posts');
- await expect(page.locator('li')).toHaveCount(2);
- await expect(page.locator('li')).toContainText([
- 'firestore item',
- 'firestore item'
- ]);
-});
+
+ test('Renders a single document', async () => {
+ await expect(page.getByTestId('doc-data')).toContainText('Hi Mom');
+ });
+
+ test('Renders a collection of items for an authenticated user in realtime', async () => {
+ await page.getByRole('button', { 'name': 'Sign In'}).click();
+ await expect(page.getByTestId('count')).toContainText('0 posts');
+ await page.getByRole('button', { name: 'Add Post' }).click();
+ await expect(page.getByTestId('count')).toContainText('1 posts');
+ await page.getByRole('button', { name: 'Add Post' }).click();
+ await expect(page.getByTestId('count')).toContainText('2 posts');
+ await expect(page.locator('li')).toHaveCount(2);
+ await expect(page.locator('li')).toContainText([
+ 'firestore item',
+ 'firestore item'
+ ]);
+ });
+
+ test('An error occurs when ', async () => {
+ await expect(page.getByTestId('error')).toContainText('Error occured');
+ });
+});
\ No newline at end of file
diff --git a/tests/storage.test.ts b/tests/storage.test.ts
index 4e5e523..b3a6211 100644
--- a/tests/storage.test.ts
+++ b/tests/storage.test.ts
@@ -1,16 +1,31 @@
-import { expect, test } from '@playwright/test';
+import { expect, test, type Page } from "@playwright/test";
+test.describe.serial("Storage", () => {
+ let page: Page;
+ test.beforeAll(async ({ browser }) => {
+ page = await browser.newPage();
+ await page.goto("/storage-test");
+ });
-test('Renders download links', async ({ page }) => {
- await page.goto('/storage-test');
- await page.waitForSelector('[data-testid="download-link"]');
- const linksCount = await page.getByTestId('download-link').count()
- expect( linksCount ).toBeGreaterThan(0);
-});
+ test.afterAll(async () => {
+ await page.close();
+ });
-test('Uploads a file', async ({ page }) => {
- await page.goto('/storage-test');
- await page.getByRole('button', { name: 'Make File' }).click();
- await expect(page.getByTestId('progress')).toContainText('100% uploaded');
- await expect(page.getByTestId('download-link2')).toContainText('test-upload.txt');
+ test("Renders download links", async () => {
+ await page.waitForSelector('[data-testid="download-link"]');
+ const linksCount = await page.getByTestId("download-link").count();
+ expect(linksCount).toBeGreaterThan(0);
+ });
+
+ test("Uploads a file", async () => {
+ await page.getByRole("button", { name: "Make File" }).click();
+ await expect(page.getByTestId("progress")).toContainText("100% uploaded");
+ await expect(page.getByTestId("download-link2")).toContainText(
+ "test-upload.txt"
+ );
+ });
+
+ test("Handle error", async () => {
+ await expect(page.getByTestId("download-url-error")).toContainText("storage/object-not-found");
+ });
});
\ No newline at end of file