@edgedev/firebase 1.0.28 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +249 -0
  2. package/index.ts +152 -12
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # @edgedev/firebase
2
+
3
+ This a collection of vue3 composables of firebase functions
4
+
5
+ ### Table of Contents
6
+ **[Installation](#installation)**
7
+ **[Firebase Authentication](#firebase-authentication)**
8
+ **[Firestore Basic Document Interactions](#firestore-Basic-document-interactions)**
9
+ **[Firestore Snapshot Listeners](#firestore-snapshot-listeners)**
10
+ **[Firestore Static Collection Data](#firestore-static-collection-data)**
11
+
12
+ # Installation
13
+
14
+ pnpm install @edgedev/firebase
15
+
16
+ ```bash
17
+ pnpm install @edgedev/firebase
18
+ ```
19
+ If installing into a Nuxt 3 project, you can make this globally avaiable by adding a file (whatever.ts) to your "composables" folder with the code below.
20
+
21
+ ```typescript
22
+ import * as edgeFirebase from "@edgedev/firebase";
23
+ export { edgeFirebase };
24
+ ```
25
+
26
+ Also or Nuxt 3 SSR must be disabled, update the nuxt.config.ts file:
27
+ ```javascript
28
+ export default defineNuxtConfig({ ssr: false });
29
+ ```
30
+
31
+ If not in Nuxt 3 or there is no need for it to be global, put this in your components <script setup>
32
+
33
+ ```javascript
34
+ <script setup>
35
+ import * as edgeFirebase from "@edgedev/firebase";
36
+ </script>
37
+ ```
38
+ <a name="Firebase Authentication>
39
+ # Firebase Authentication
40
+ (currently only sign in with email and password is supported)
41
+
42
+ If "persistence" is true, login will be saved locally, they can close their browser and when they open they will be logged in automatically. If "persistence" is false login saved only for the session.
43
+ ```javascript
44
+ edgeFirebase.logIn(
45
+ {
46
+ email: "devs@edgemarketing.com",
47
+ password: "pasword"
48
+ },
49
+ true // : persistence
50
+ );
51
+ ```
52
+
53
+ #### User information is contained in: edgeFirebase.user
54
+ The user object is reactive and contains these items:
55
+ ```typescript
56
+ interface UserDataObject {
57
+ uid: string | null;
58
+ email: string;
59
+ loggedIn: boolean;
60
+ logInError: boolean;
61
+ logInErrorMessage: string;
62
+ }
63
+ ```
64
+ The reactive item **edgeFirebase.user.loggedIn** can be used in code or templates to determine if they user is logged in.
65
+
66
+ If there is an error logging in, **edgeFirebase.user.logInError** will be true and **edgeFirebase.user.logInErrorMessage** can be used to return that error to the user.
67
+
68
+ After logging in, **edgeFirebase.logOut** becomes available. Logging out will also automatically disconnect all FireStore listeners.
69
+
70
+ Here is a sample component using the login:
71
+ ```html
72
+ <template>
73
+ <div>
74
+ <div v-if="edgeFirebase.user.loggedIn">
75
+ <button @click="edgeFirebase.logOut">Logout</button><br />
76
+ <AppUsers v-if="edgeFirebase.user.loggedIn" />
77
+ </div>
78
+ <div v-else>
79
+ <input v-model="email" style="width: 400px" type="text" /><br />
80
+ <input v-model="password" style="width: 400px" type="text" /><br />
81
+ <button @click="login">Login</button><br />
82
+ <div v-if="edgeFirebase.user.logInError">
83
+ {{ edgeFirebase.user.logInErrorMessage }}
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </template>
88
+ ```
89
+
90
+ ```javascript
91
+ <script setup>
92
+ const email = ref("");
93
+ const password = ref("");
94
+ const login = () => {
95
+ edgeFirebase.logIn(
96
+ {
97
+ email: email.value,
98
+ password: password.value
99
+ },
100
+ true
101
+ );
102
+ };
103
+ </script>
104
+ ```
105
+ # Firestore Basic Document Interactions
106
+ ### Adding/Update a Document.
107
+ Both adding and updating a document use the same function: **edgeFirebase.storeDoc(collectionPath, object)** for a document to be updated the object must contain the key **docId** and the value must match the ID of a document in the collection on are updating *(Note: All documents returned by edgeFirebase functions will already have docId insert in the document objects)*. If the object does not contain docId or the docId doesn't match a document in the collection, new document will be created.
108
+
109
+ ```javascript
110
+ <script setup>
111
+ const addUser = {name: "bob"};
112
+ edgeFirebase.storeDoc("users", addUser);
113
+ </script>
114
+ ```
115
+ Note: When a document is written to the collection several other keys are added that can be referenced: **doc_created_at**(timestamp of doc creation), **last_updated**(timestamp document last written), **uid**(the user id of the user that updated or created the document).
116
+
117
+ ### Getting a single Document.
118
+ If you want to query a single document from a collection use: **edgeFirebase.getDocData(collectionPath, docId)**
119
+ ```javascript
120
+ <script setup>
121
+ const docId = "DrJRpDXVsEEqZu0UB8NT";
122
+ const singleDoc = edgeFirebase.getDocData("users", docId);
123
+ </script>
124
+ ```
125
+
126
+ ### Deleting a Document.
127
+ To delete a document use: **edgeFirebase.removeDoc(collectionPath, docId)**
128
+ ```javascript
129
+ <script setup>
130
+ const docId = "DrJRpDXVsEEqZu0UB8NT";
131
+ const singleDoc = edgeFirebase.removeDoc("users", docId);
132
+ </script>
133
+ ```
134
+
135
+ # Firestore Snapshot Listeners
136
+ ### Starting a snapshot listener on a collection.
137
+ To start a snapshot listen on a collection use: **edgeFirebase.startSnapshot(collectionPath)**
138
+ ```javascript
139
+ <script setup>
140
+ edgeFirebase.startSnapshot("users");
141
+ </script>
142
+ ```
143
+ Once you have started a snapshot reactive data for that snapshot will be available with **edgeFirebase.data[collectionPath]**. Each document in the data object is keyed with the DocumentId from FireStore.
144
+ ```html
145
+ <template>
146
+ <div>
147
+ <div v-for="item in edgeFirebase.data.users" :key="item">
148
+ {{ item.name }}
149
+ </div>
150
+ </div>
151
+ </template>
152
+ ```
153
+ ### Snapshot listeners can also be queried, sorted, and limited.
154
+ #### Query and Sort are an array of objects, Limit is a number
155
+ (if passing more than one query on different keys, FireStore may make you create indexes)
156
+ ```typescript
157
+ interface FirestoreQuery {
158
+ field: string;
159
+ operator: WhereFilterOp; // '==' | '<' | '<=' | '>' | '>=' | 'array-contains' | 'in' | 'array-contains-any';
160
+ value: unknown;
161
+ }
162
+
163
+ interface FirestoreOrderBy {
164
+ field: string;
165
+ direction: "asc" | "desc";
166
+ }
167
+ ```
168
+ ##### Example with query, sort and limit:
169
+ ```javascript
170
+ <script setup>
171
+ const query = [{field: "name", operator: "==", value="Bob"}];
172
+ const sort = [{ field: "name", direction: "asc" }];
173
+ const limit = 10;
174
+ edgeFirebase.startSnapshot("users", query, sort, limit);
175
+ </setup>
176
+ ```
177
+ ### Stopping a snapshot listener
178
+ To stop listening to a collection use: **edgeFirebase.stopSnapshot(collectionPath)**
179
+ ```javascript
180
+ <script setup>
181
+ edgeFirebase.stopSnapshot("users");
182
+ </setup>
183
+ ```
184
+
185
+ # Firestore Static Collection Data
186
+ To get static data from a collection use the Object: **edgeFirebase.SearchStaticData()**. Static search is done from a class to handle pagination better.
187
+ ```javascript
188
+ const staticSearch = new edgeFirebase.SearchStaticData();
189
+ staticSearch.getData("users");
190
+ ```
191
+ After initialized like above... Data will be available from **staticSearch.results.data**
192
+
193
+ ### The static data object can also be queried, sorted, limited and paginated.
194
+ (if passing more than one query on different keys, FireStore may make you create indexes)
195
+ ```typescript
196
+ interface FirestoreQuery {
197
+ field: string;
198
+ operator: WhereFilterOp; // '==' | '<' | '<=' | '>' | '>=' | 'array-contains' | 'in' | 'array-contains-any';
199
+ value: unknown;
200
+ }
201
+
202
+ interface FirestoreOrderBy {
203
+ field: string;
204
+ direction: "asc" | "desc";
205
+ }
206
+ ```
207
+
208
+ ### Pagination
209
+
210
+ For pagination purposes there are 2 functions **staticSearch.next()** and **staticSearch.prev()**
211
+ for updating **staticSearch.results.data** the pagination data set. There are also two helper variables **staticSearch.results.staticIsFirstPage** (set to true if the data is at the first pagination data set) and **staticSearch.results.staticIsLastPage** (set to true if the data is on the last pagination data set). Note: Because of the way Firestore pagination works, you don't know you are at your last data set until you try and query for the next. If you are using using **staticSearch.results.staticIsLastPage** to disable a "Next" button for example it won't happen until the "second" click and in that scenario **staticSearch.results.data** will just remain at the last pagination data set, it won't break.
212
+
213
+ ### Example - Template and code with query, sort, limit, and pagination:
214
+ ```html
215
+ <template>
216
+ <div>
217
+ <div v-for="item in staticSearch.results.data" :key="item">
218
+ {{ item.name }}
219
+ </div>
220
+ <div>
221
+ <button
222
+ v-if="!staticSearch.results.staticIsFirstPage"
223
+ @click="staticSearch.prev()"
224
+ >
225
+ Previous
226
+ </button>
227
+ <button
228
+ v-if="!staticSearch.results.staticIsLastPage"
229
+ @click="staticSearch.next()"
230
+ >
231
+ Next
232
+ </button>
233
+ </div>
234
+ </div>
235
+ </template>
236
+ ```
237
+ ```javascript
238
+ <script setup>
239
+ const staticSearch = new edgeFirebase.SearchStaticData();
240
+
241
+ const query = [{field: "name", operator: "==", value="Bob"}];
242
+ const sort = [{ field: "name", direction: "asc" }];
243
+ const limit = 10;
244
+
245
+ staticSearch.getData("users", query, sort, limit);
246
+ </script>
247
+ ```
248
+ ## License
249
+ [ISC](https://choosealicense.com/licenses/isc/)
package/index.ts CHANGED
@@ -19,8 +19,8 @@ import {
19
19
  orderBy,
20
20
  limit,
21
21
  Query,
22
- startAt,
23
- startAfter
22
+ startAfter,
23
+ DocumentData
24
24
  } from "firebase/firestore";
25
25
 
26
26
  import {
@@ -70,6 +70,11 @@ interface Credentials {
70
70
  password: string;
71
71
  }
72
72
 
73
+ interface StaticDataResult {
74
+ data: object;
75
+ next: DocumentData | null;
76
+ }
77
+
73
78
  const firebaseConfig = {
74
79
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY as string,
75
80
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string,
@@ -184,30 +189,165 @@ export const getStaticData = async (
184
189
  collectionPath: string,
185
190
  queryList: FirestoreQuery[] = [],
186
191
  orderList: FirestoreOrderBy[] = [],
187
- max: 0,
188
- after = ""
189
- ): Promise<{ [key: string]: unknown }> => {
190
- const data: { [key: string]: unknown } = {};
191
- const q = getQuery(collectionPath, queryList, orderList, max, after);
192
+ max = 0,
193
+ last: DocumentData | null = null
194
+ ): Promise<StaticDataResult> => {
195
+ const data: object = {};
196
+
197
+ const q = getQuery(collectionPath, queryList, orderList, max, last);
198
+
192
199
  const docs = await getDocs(q);
200
+ const nextLast: DocumentData = docs.docs[docs.docs.length - 1];
201
+
193
202
  docs.forEach((doc) => {
194
203
  const item = doc.data();
195
204
  item.docId = doc.id;
196
205
  data[doc.id] = item;
197
206
  });
198
- return data;
207
+ return { data, next: nextLast };
199
208
  };
200
209
 
210
+ // Class for wrapping a getSaticData to handle pagination
211
+ export class SearchStaticData {
212
+ collectionPath = "";
213
+ queryList: FirestoreQuery[] = [];
214
+ orderList: FirestoreOrderBy[] = [];
215
+ max = 0;
216
+
217
+ results = reactive({
218
+ data: {},
219
+ pagination: [],
220
+ staticIsLastPage: true,
221
+ staticIsFirstPage: true,
222
+ staticCurrentPage: ""
223
+ });
224
+
225
+ prev = async (): Promise<void> => {
226
+ const findIndex = this.results.pagination.findIndex(
227
+ (x) => x.key === this.results.staticCurrentPage
228
+ );
229
+ let last = null;
230
+ if (findIndex === 1) {
231
+ this.results.staticCurrentPage = "";
232
+ this.results.staticIsLastPage = false;
233
+ this.results.staticIsFirstPage = true;
234
+ } else {
235
+ last = this.results.pagination[findIndex - 2].next;
236
+ this.results.staticCurrentPage =
237
+ this.results.pagination[findIndex - 2].key;
238
+ }
239
+ await this.afterNextPrev(last);
240
+ };
241
+
242
+ next = async (): Promise<void> => {
243
+ const findIndex = this.results.pagination.findIndex(
244
+ (x) => x.key === this.results.staticCurrentPage
245
+ );
246
+ const last = this.results.pagination[findIndex].next;
247
+ if (this.results.pagination.length === 1) {
248
+ this.results.staticIsFirstPage = true;
249
+ } else {
250
+ this.results.staticIsFirstPage = false;
251
+ }
252
+ await this.afterNextPrev(last);
253
+ };
254
+
255
+ afterNextPrev = async (last): Promise<void> => {
256
+ let results = await getStaticData(
257
+ "users",
258
+ this.queryList,
259
+ this.orderList,
260
+ this.max,
261
+ last
262
+ );
263
+
264
+ if (last && Object.keys(results.data).length === 0) {
265
+ this.results.staticIsLastPage = true;
266
+ if (this.results.pagination.length === 1) {
267
+ last = null;
268
+ this.results.staticCurrentPage = "";
269
+ this.results.staticIsFirstPage = true;
270
+ } else {
271
+ last = this.results.pagination[this.results.pagination.length - 2].next;
272
+ this.results.staticCurrentPage =
273
+ this.results.pagination[this.results.pagination.length - 2].key;
274
+ }
275
+ results = await getStaticData(
276
+ "users",
277
+ this.queryList,
278
+ this.orderList,
279
+ this.max,
280
+ last
281
+ );
282
+ } else {
283
+ this.results.staticIsLastPage = false;
284
+ if (this.results.pagination.length === 1) {
285
+ this.results.staticIsFirstPage = false;
286
+ }
287
+ }
288
+ this.results.data = results.data;
289
+ this.results.staticCurrentPage = results.next.id;
290
+ if (!this.results.staticIsLastPage) {
291
+ if (results.next) {
292
+ const findItem = this.results.pagination.find(
293
+ (x) => x.key === results.next.id
294
+ );
295
+ if (!findItem) {
296
+ this.results.pagination.push({
297
+ key: results.next.id,
298
+ next: results.next
299
+ });
300
+ }
301
+ }
302
+ }
303
+ };
304
+
305
+ getData = async (
306
+ collectionPath: string,
307
+ queryList: FirestoreQuery[] = [],
308
+ orderList: FirestoreOrderBy[] = [],
309
+ max = 0
310
+ ): Promise<void> => {
311
+ this.collectionPath = collectionPath;
312
+ this.queryList = queryList;
313
+ this.orderList = orderList;
314
+ this.max = max;
315
+ this.results.staticIsLastPage = true;
316
+ this.results.staticIsFirstPage = true;
317
+ this.results.staticCurrentPage = "";
318
+ this.results.pagination = [];
319
+ this.results.pagination = [];
320
+ this.results.data = {};
321
+ const results = await getStaticData(
322
+ collectionPath,
323
+ queryList,
324
+ orderList,
325
+ max
326
+ );
327
+ if (Object.keys(results.data).length > 0) {
328
+ this.results.staticIsLastPage = false;
329
+ this.results.data = results.data;
330
+ this.results.staticCurrentPage = results.next.id;
331
+ this.results.pagination.push({
332
+ key: results.next.id,
333
+ next: results.next
334
+ });
335
+ } else {
336
+ this.results.staticIsLastPage = true;
337
+ this.results.staticIsFirstPage = true;
338
+ }
339
+ };
340
+ }
341
+
201
342
  // Composable to start snapshot listener and set unsubscribe function
202
343
  export const startSnapshot = (
203
344
  collectionPath: string,
204
345
  queryList: FirestoreQuery[] = [],
205
346
  orderList: FirestoreOrderBy[] = [],
206
- max = 0,
207
- after = ""
347
+ max = 0
208
348
  ): void => {
209
349
  data[collectionPath] = {};
210
- const q = getQuery(collectionPath, queryList, orderList, max, after);
350
+ const q = getQuery(collectionPath, queryList, orderList, max);
211
351
  const unsubscribe = onSnapshot(q, (querySnapshot) => {
212
352
  const items = {};
213
353
  querySnapshot.forEach((doc) => {
@@ -225,7 +365,7 @@ const getQuery = (
225
365
  queryList: FirestoreQuery[] = [],
226
366
  orderList: FirestoreOrderBy[] = [],
227
367
  max = 0,
228
- after = ""
368
+ after: DocumentData | null = null
229
369
  ): Query => {
230
370
  const queryConditions: QueryConstraint[] = queryList.map((condition) =>
231
371
  where(condition.field, condition.operator, condition.value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/firebase",
3
- "version": "1.0.28",
3
+ "version": "1.1.1",
4
4
  "description": "Composables and stores for firebase",
5
5
  "main": "index.ts",
6
6
  "author": "Seth Fischer",