@speakableio/core 0.1.0 → 0.1.2

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.
package/dist/index.mjs CHANGED
@@ -1,36 +1,290 @@
1
1
  // src/providers/SpeakableProvider.tsx
2
2
  import { createContext, useContext, useEffect, useState } from "react";
3
3
 
4
+ // src/utils/error-handler.ts
5
+ var ServiceError = class extends Error {
6
+ constructor(message, originalError, code) {
7
+ super(message);
8
+ this.originalError = originalError;
9
+ this.code = code;
10
+ this.name = "ServiceError";
11
+ }
12
+ };
13
+ function withErrorHandler(fn, serviceName) {
14
+ return async (...args) => {
15
+ try {
16
+ return await fn(...args);
17
+ } catch (error) {
18
+ if (error instanceof Error && "code" in error) {
19
+ const firebaseError = error;
20
+ throw new ServiceError(
21
+ `Error in ${serviceName}: ${firebaseError.message}`,
22
+ error,
23
+ firebaseError.code
24
+ );
25
+ }
26
+ if (error instanceof Error) {
27
+ throw new ServiceError(`Error in ${serviceName}: ${error.message}`, error);
28
+ }
29
+ throw new ServiceError(`Unknown error in ${serviceName}`, error);
30
+ }
31
+ };
32
+ }
33
+
34
+ // src/lib/firebase/api.ts
35
+ var FirebaseAPI = class _FirebaseAPI {
36
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
37
+ constructor() {
38
+ this.config = null;
39
+ }
40
+ static getInstance() {
41
+ if (!_FirebaseAPI.instance) {
42
+ _FirebaseAPI.instance = new _FirebaseAPI();
43
+ }
44
+ return _FirebaseAPI.instance;
45
+ }
46
+ initialize(config) {
47
+ this.config = config;
48
+ }
49
+ get db() {
50
+ if (!this.config) throw new Error("FirebaseAPI not initialized");
51
+ return this.config.db;
52
+ }
53
+ get helpers() {
54
+ if (!this.config) throw new Error("FirebaseAPI not initialized");
55
+ return this.config.helpers;
56
+ }
57
+ accessQueryConstraints() {
58
+ const { query, orderBy, limit, startAt, startAfter, endAt, endBefore } = this.helpers;
59
+ return {
60
+ query,
61
+ orderBy,
62
+ limit,
63
+ startAt,
64
+ startAfter,
65
+ endAt,
66
+ endBefore
67
+ };
68
+ }
69
+ async getDoc(path) {
70
+ const { getDoc, doc } = this.helpers;
71
+ const docRef = doc(this.db, path);
72
+ const docSnap = await getDoc(docRef);
73
+ return {
74
+ id: docSnap.id,
75
+ data: docSnap.exists() ? docSnap.data() : null
76
+ };
77
+ }
78
+ async getDocs(path, ...queryConstraints) {
79
+ const { getDocs, query, collection } = this.helpers;
80
+ const collectionRef = collection(this.db, path);
81
+ const q = queryConstraints.length > 0 ? query(collectionRef, ...queryConstraints) : collectionRef;
82
+ const querySnapshot = await getDocs(q);
83
+ const data = querySnapshot.docs.map((doc) => ({
84
+ id: doc.id,
85
+ data: doc.data()
86
+ }));
87
+ return {
88
+ data,
89
+ querySnapshot
90
+ };
91
+ }
92
+ async addDoc(path, data) {
93
+ const { addDoc, collection } = this.helpers;
94
+ const collectionRef = collection(this.db, path);
95
+ const docRef = await addDoc(collectionRef, data);
96
+ return docRef.id;
97
+ }
98
+ async setDoc(path, data) {
99
+ const { setDoc, doc } = this.helpers;
100
+ const docRef = doc(this.db, path);
101
+ await setDoc(docRef, data);
102
+ }
103
+ async updateDoc(path, data) {
104
+ const { updateDoc, doc } = this.helpers;
105
+ const docRef = doc(this.db, path);
106
+ await updateDoc(docRef, data);
107
+ }
108
+ async deleteDoc(path) {
109
+ const { deleteDoc, doc } = this.helpers;
110
+ const docRef = doc(this.db, path);
111
+ await deleteDoc(docRef);
112
+ }
113
+ async runTransaction(updateFunction) {
114
+ const { runTransaction } = this.helpers;
115
+ return runTransaction(this.db, updateFunction);
116
+ }
117
+ async runBatch(operations) {
118
+ const { writeBatch } = this.helpers;
119
+ const batch = writeBatch(this.db);
120
+ await Promise.all(operations.map((op) => op()));
121
+ await batch.commit();
122
+ }
123
+ };
124
+ var api = FirebaseAPI.getInstance();
125
+
126
+ // src/domains/assignment/assignment.constants.ts
127
+ var ASSIGNMENT_ANALYTICS_TYPES = [
128
+ "macro" /* Macro */,
129
+ "gradebook" /* Gradebook */,
130
+ "cards" /* Cards */,
131
+ "student" /* Student */,
132
+ "student_summary" /* StudentSummary */
133
+ ];
134
+ var ASSIGNMENTS_COLLECTION = "assignments";
135
+ var ANALYTICS_SUBCOLLECTION = "analytics";
136
+ var SCORES_SUBCOLLECTION = "scores";
137
+ var refsAssignmentFiresotre = {
138
+ allAssignments: () => ASSIGNMENTS_COLLECTION,
139
+ assignment: (id) => `${ASSIGNMENTS_COLLECTION}/${id}`,
140
+ assignmentAllAnalytics: (id) => `${ASSIGNMENTS_COLLECTION}/${id}/${ANALYTICS_SUBCOLLECTION}`,
141
+ assignmentAnalytics: (id, type) => `${ASSIGNMENTS_COLLECTION}/${id}/${ANALYTICS_SUBCOLLECTION}/${type}`,
142
+ assignmentScores: (id, userId) => `${ASSIGNMENTS_COLLECTION}/${id}/${SCORES_SUBCOLLECTION}/${userId}`
143
+ };
144
+
145
+ // src/domains/assignment/services/get-assignments-score.service.ts
146
+ var _getAssignmentScores = async ({
147
+ assignmentId,
148
+ analyticType = "macro" /* Macro */,
149
+ studentId,
150
+ currentUserId
151
+ }) => {
152
+ if (analyticType === "student" /* Student */) {
153
+ const path = refsAssignmentFiresotre.assignmentScores(assignmentId, currentUserId);
154
+ const response = await api.getDoc(path);
155
+ return { scores: response.data, id: assignmentId };
156
+ }
157
+ if (analyticType === "student_summary" /* StudentSummary */ && studentId) {
158
+ const path = refsAssignmentFiresotre.assignmentScores(assignmentId, studentId);
159
+ const response = await api.getDoc(path);
160
+ return { scores: response.data, id: assignmentId };
161
+ }
162
+ if (analyticType !== "all" /* All */ && ASSIGNMENT_ANALYTICS_TYPES.includes(analyticType)) {
163
+ const ref = refsAssignmentFiresotre.assignmentAnalytics(assignmentId, analyticType);
164
+ const docData = await api.getDoc(ref);
165
+ return { scores: docData.data, id: assignmentId };
166
+ } else if (analyticType === "all" /* All */) {
167
+ const ref = refsAssignmentFiresotre.assignmentAllAnalytics(assignmentId);
168
+ const response = await api.getDocs(ref);
169
+ const data = response.data.reduce((acc, curr) => {
170
+ acc[curr.id] = curr;
171
+ return acc;
172
+ }, {});
173
+ return { scores: data, id: assignmentId };
174
+ }
175
+ };
176
+ var getAssignmentScores = withErrorHandler(_getAssignmentScores, "getAssignmentScores");
177
+
178
+ // src/domains/assignment/services/attach-score-assignment.service.ts
179
+ var _attachScoresAssignment = async ({
180
+ assignments,
181
+ analyticType,
182
+ studentId,
183
+ currentUserId
184
+ }) => {
185
+ const scoresPromises = assignments.map((a) => {
186
+ return getAssignmentScores({
187
+ assignmentId: a.id,
188
+ analyticType,
189
+ studentId,
190
+ currentUserId
191
+ });
192
+ });
193
+ const scores = await Promise.all(scoresPromises);
194
+ const scoresObject = scores.reduce((acc, curr) => {
195
+ acc[curr.id] = curr.scores;
196
+ return acc;
197
+ }, {});
198
+ const assignmentsWithScores = assignments.map((a) => {
199
+ return {
200
+ ...a,
201
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/ban-ts-comment
202
+ // @ts-ignore
203
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
204
+ scores: scoresObject[a.id] ?? null
205
+ };
206
+ });
207
+ return assignmentsWithScores;
208
+ };
209
+ var attachScoresAssignment = withErrorHandler(
210
+ _attachScoresAssignment,
211
+ "attachScoresAssignment"
212
+ );
213
+
214
+ // src/domains/assignment/services/get-all-assignment.service.ts
215
+ async function _getAllAssignments() {
216
+ const path = refsAssignmentFiresotre.allAssignments();
217
+ const response = await api.getDocs(path);
218
+ return response.data;
219
+ }
220
+ var getAllAssignments = withErrorHandler(_getAllAssignments, "getAllAssignments");
221
+
222
+ // src/domains/assignment/utils/check-assignment-availability.ts
223
+ import dayjs from "dayjs";
224
+ var checkAssignmentAvailability = (scheduledTime) => {
225
+ if (!scheduledTime) return true;
226
+ const scheduledDate = typeof scheduledTime === "string" ? dayjs(scheduledTime) : dayjs(scheduledTime.toDate());
227
+ if (!scheduledDate.isValid()) return true;
228
+ return dayjs().isAfter(scheduledDate);
229
+ };
230
+
231
+ // src/domains/assignment/services/get-assignment.service.ts
232
+ async function _getAssignment(params) {
233
+ const path = refsAssignmentFiresotre.assignment(params.assignmentId);
234
+ const response = await api.getDoc(path);
235
+ if (!response.data) return null;
236
+ const assignment = response.data;
237
+ const isAvailable = checkAssignmentAvailability(assignment.scheduledTime);
238
+ const assignmentWithId = {
239
+ ...assignment,
240
+ isAvailable,
241
+ id: params.assignmentId,
242
+ scheduledTime: assignment.scheduledTime ?? null
243
+ };
244
+ if (params.analyticType) {
245
+ const assignmentsWithScores = await attachScoresAssignment({
246
+ assignments: [assignmentWithId],
247
+ analyticType: params.analyticType,
248
+ currentUserId: params.currentUserId
249
+ });
250
+ return assignmentsWithScores.length > 0 ? assignmentsWithScores[0] : assignmentWithId;
251
+ }
252
+ return assignmentWithId;
253
+ }
254
+ var getAssignment = withErrorHandler(_getAssignment, "getAssignment");
255
+
4
256
  // src/domains/assignment/assignment.repo.ts
5
- var createAssignmentRepo = (db, h) => {
6
- const { doc, collection, getDoc, getDocs } = h;
257
+ var createAssignmentRepo = () => {
7
258
  return {
8
- async get(id) {
9
- const snap = await getDoc(doc(db, `assignments/${id}`));
10
- if (!snap.exists()) return null;
11
- const data = snap.data();
12
- return { id, ...data };
13
- },
14
- async list() {
15
- const col = collection(db, "assignments");
16
- const snap = await getDocs(col);
17
- return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
18
- }
259
+ getAssignment,
260
+ attachScoresAssignment,
261
+ getAssignmentScores,
262
+ getAllAssignments
19
263
  };
20
264
  };
21
265
 
22
- // src/domains/assignment/hooks.ts
266
+ // src/domains/assignment/assignment.hooks.ts
23
267
  import { useQuery } from "@tanstack/react-query";
24
268
  var assignmentQueryKeys = {
25
269
  all: ["assignments"],
26
270
  byId: (id) => [...assignmentQueryKeys.all, id],
27
271
  list: () => [...assignmentQueryKeys.all, "list"]
28
272
  };
29
- function useAssignment(id) {
30
- const { fs } = useFs();
273
+ function useAssignment({
274
+ assignmentId,
275
+ enabled = true,
276
+ analyticType,
277
+ userId
278
+ }) {
279
+ const { speakableApi } = useSpeakableApi();
31
280
  return useQuery({
32
- queryKey: assignmentQueryKeys.byId(id),
33
- queryFn: () => fs.assignmentRepo.get(id)
281
+ queryKey: assignmentQueryKeys.byId(assignmentId),
282
+ queryFn: () => speakableApi.assignmentRepo.getAssignment({
283
+ assignmentId,
284
+ analyticType,
285
+ currentUserId: userId
286
+ }),
287
+ enabled
34
288
  });
35
289
  }
36
290
 
@@ -38,8 +292,12 @@ function useAssignment(id) {
38
292
  async function createFsClient(db, platform) {
39
293
  const dbAsFirestore = db;
40
294
  const helpers = platform === "web" ? await import("firebase/firestore") : await import("@react-native-firebase/firestore");
295
+ api.initialize({
296
+ db: dbAsFirestore,
297
+ helpers
298
+ });
41
299
  return {
42
- assignmentRepo: createAssignmentRepo(dbAsFirestore, helpers)
300
+ assignmentRepo: createAssignmentRepo()
43
301
  };
44
302
  }
45
303
 
@@ -52,27 +310,27 @@ function SpeakableProvider({
52
310
  children,
53
311
  queryClient
54
312
  }) {
55
- const [fs, setFs] = useState(null);
313
+ const [speakableApi, setSpeakableApi] = useState(null);
56
314
  useEffect(() => {
57
315
  createFsClient(db, platform).then((repos) => {
58
- setFs(repos);
316
+ setSpeakableApi(repos);
59
317
  });
60
318
  }, [db, platform]);
61
- if (!fs) return null;
319
+ if (!speakableApi) return null;
62
320
  return /* @__PURE__ */ jsx(
63
321
  FsCtx.Provider,
64
322
  {
65
323
  value: {
66
- fs,
324
+ speakableApi,
67
325
  queryClient
68
326
  },
69
327
  children
70
328
  }
71
329
  );
72
330
  }
73
- function useFs() {
331
+ function useSpeakableApi() {
74
332
  const ctx = useContext(FsCtx);
75
- if (!ctx) throw new Error("useFs must be used within a SpeakableProvider");
333
+ if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
76
334
  return ctx;
77
335
  }
78
336
  export {
@@ -82,5 +340,5 @@ export {
82
340
  createAssignmentRepo,
83
341
  createFsClient,
84
342
  useAssignment,
85
- useFs
343
+ useSpeakableApi
86
344
  };
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@speakableio/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Speakable-io/speakable-core.git"
7
7
  },
8
- "main": "dist/index.cjs",
9
- "module": "dist/index.js",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --dts --format esm,cjs --splitting",
10
+ "dev": "tsup src/index.ts --watch --dts --format esm,cjs --out-dir dist",
11
+ "test": "vitest"
12
+ },
13
+ "react-native": "dist/index.js",
14
+ "main": "dist/index.js",
15
+ "module": "dist/index.mjs",
10
16
  "devDependencies": {
11
17
  "react": "^18.2.0",
12
18
  "@types/react": "^18",
@@ -36,22 +42,26 @@
36
42
  "@tanstack/react-query": "^5.54.1"
37
43
  },
38
44
  "exports": {
39
- ".": "./dist/index.js"
45
+ ".": {
46
+ "react-native": "./dist/index.js",
47
+ "require": "./dist/index.js",
48
+ "import": "./dist/index.mjs"
49
+ }
40
50
  },
41
51
  "bugs": {
42
52
  "url": "https://github.com/Speakable-io/speakable-core/issues"
43
53
  },
44
54
  "description": "Shared data layer for Speakable web & mobile",
45
- "files": ["dist"],
55
+ "files": [
56
+ "dist"
57
+ ],
46
58
  "homepage": "https://github.com/Speakable-io/speakable-core#readme",
47
59
  "license": "UNLICENSED",
48
60
  "publishConfig": {
49
61
  "access": "restricted"
50
62
  },
51
- "scripts": {
52
- "build": "tsup src/index.ts --dts --format esm,cjs --splitting",
53
- "dev": "tsup src/index.ts --watch --dts --format esm,cjs --out-dir dist",
54
- "test": "vitest"
55
- },
56
- "types": "dist/index.d.ts"
63
+ "types": "dist/index.d.ts",
64
+ "dependencies": {
65
+ "dayjs": "^1.11.13"
66
+ }
57
67
  }