@lasterp/shared 1.0.0-alpha.2 → 1.0.0-alpha.4

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.
@@ -0,0 +1,496 @@
1
+ // src/frappe/provider.tsx
2
+ import { useMemo, createContext } from "react";
3
+ import { FrappeApp } from "frappe-js-sdk";
4
+
5
+ // src/frappe/socket.ts
6
+ import io from "socket.io-client";
7
+
8
+ class SocketIO {
9
+ socket_port;
10
+ host;
11
+ port;
12
+ protocol;
13
+ url;
14
+ site_name;
15
+ socket;
16
+ constructor(url, site_name, socket_port, tokenParams) {
17
+ this.socket_port = socket_port ?? "9000";
18
+ this.host = window.location?.hostname;
19
+ this.port = window.location?.port ? `:${this.socket_port}` : "";
20
+ this.protocol = window.location?.protocol === "https:" ? "https" : "http";
21
+ if (url) {
22
+ let urlObject = new URL(url);
23
+ urlObject.port = "";
24
+ if (socket_port) {
25
+ urlObject.port = socket_port;
26
+ this.url = urlObject.toString();
27
+ } else {
28
+ this.url = urlObject.toString();
29
+ }
30
+ } else {
31
+ this.url = `${this.protocol}://${this.host}${this.port}/`;
32
+ }
33
+ if (site_name) {
34
+ this.url = `${this.url}${site_name}`;
35
+ }
36
+ this.site_name = site_name;
37
+ this.socket = io(`${this.url}`, {
38
+ withCredentials: true,
39
+ secure: this.protocol === "https",
40
+ extraHeaders: tokenParams && tokenParams.useToken === true ? {
41
+ Authorization: `${tokenParams.type} ${tokenParams.token?.()}`
42
+ } : {}
43
+ });
44
+ }
45
+ }
46
+
47
+ // src/frappe/provider.tsx
48
+ import { jsxDEV } from "react/jsx-dev-runtime";
49
+ var FrappeContext = createContext(null);
50
+ var FrappeProvider = ({
51
+ url = "",
52
+ tokenParams,
53
+ enableSocket = true,
54
+ socketPort,
55
+ siteName,
56
+ children,
57
+ customHeaders
58
+ }) => {
59
+ const frappeConfig = useMemo(() => {
60
+ const frappe = new FrappeApp(url, tokenParams, undefined, customHeaders);
61
+ return {
62
+ url,
63
+ tokenParams,
64
+ app: frappe,
65
+ auth: frappe.auth(),
66
+ db: frappe.db(),
67
+ call: frappe.call(),
68
+ file: frappe.file(),
69
+ socket: enableSocket ? new SocketIO(url, siteName, socketPort, tokenParams).socket : undefined
70
+ };
71
+ }, [url, tokenParams, enableSocket, socketPort, siteName, customHeaders]);
72
+ return /* @__PURE__ */ jsxDEV(FrappeContext.Provider, {
73
+ value: frappeConfig,
74
+ children
75
+ }, undefined, false, undefined, this);
76
+ };
77
+ // src/frappe/hooks/auth.ts
78
+ import { useCallback, useContext, useState, useEffect } from "react";
79
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
80
+ var useFrappeAuth = () => {
81
+ const queryClient = useQueryClient();
82
+ const { auth, tokenParams } = useContext(FrappeContext);
83
+ const [userID, setUserID] = useState();
84
+ const getUserCookie = useCallback(() => {
85
+ const userCookie = document.cookie.split(";").find((c) => c.trim().startsWith("user_id="));
86
+ if (userCookie) {
87
+ const userName = userCookie.split("=")[1];
88
+ if (userName && userName !== "Guest") {
89
+ setUserID(userName);
90
+ } else {
91
+ setUserID(null);
92
+ }
93
+ } else {
94
+ setUserID(null);
95
+ }
96
+ }, []);
97
+ useEffect(() => {
98
+ if (tokenParams && tokenParams.useToken) {
99
+ setUserID(null);
100
+ } else {
101
+ getUserCookie();
102
+ }
103
+ }, [tokenParams, getUserCookie]);
104
+ const {
105
+ data: currentUser,
106
+ error,
107
+ isLoading,
108
+ isFetching: isValidating
109
+ } = useQuery({
110
+ queryKey: ["currentUser", { tokenParams, userID }],
111
+ queryFn: () => auth.getLoggedInUser(),
112
+ enabled: !!(tokenParams?.useToken || userID),
113
+ retry: false,
114
+ refetchOnWindowFocus: false,
115
+ staleTime: Infinity
116
+ });
117
+ const loginMutation = useMutation({
118
+ mutationFn: (credentials) => auth.loginWithUsernamePassword(credentials),
119
+ onSuccess: () => {
120
+ getUserCookie();
121
+ queryClient.invalidateQueries({ queryKey: ["currentUser"] });
122
+ }
123
+ });
124
+ const logoutMutation = useMutation({
125
+ mutationFn: () => auth.logout(),
126
+ onSuccess: () => {
127
+ setUserID(null);
128
+ queryClient.setQueryData(["currentUser"], null);
129
+ }
130
+ });
131
+ return {
132
+ isLoading: userID === undefined || isLoading,
133
+ currentUser,
134
+ isValidating,
135
+ error,
136
+ login: loginMutation.mutateAsync,
137
+ logout: logoutMutation.mutateAsync,
138
+ updateCurrentUser: () => queryClient.invalidateQueries({ queryKey: ["currentUser"] }),
139
+ getUserCookie
140
+ };
141
+ };
142
+ // src/frappe/hooks/call.ts
143
+ import { useContext as useContext2 } from "react";
144
+ import { useMutation as useMutation2, useQuery as useQuery2 } from "@tanstack/react-query";
145
+
146
+ // src/frappe/utils.ts
147
+ function encodeQueryData(data) {
148
+ const ret = [];
149
+ for (let d in data)
150
+ ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
151
+ return ret.join("&");
152
+ }
153
+
154
+ // src/frappe/hooks/call.ts
155
+ var useFrappeGetCall = (method, params, options, type = "GET") => {
156
+ const { call } = useContext2(FrappeContext);
157
+ const urlParams = encodeQueryData(params ?? {});
158
+ const url = `${method}?${urlParams}`;
159
+ const queryKey = ["frappeCall", type, method, url];
160
+ const { data, error, isLoading, isFetching, refetch } = useQuery2({
161
+ queryKey,
162
+ queryFn: type === "GET" ? () => call.get(method, params) : () => call.post(method, params),
163
+ enabled: (options?.enabled ?? true) && !!method,
164
+ staleTime: options?.staleTime,
165
+ retry: options?.retry ?? false
166
+ });
167
+ return {
168
+ data,
169
+ error,
170
+ isLoading,
171
+ isFetching,
172
+ refetch
173
+ };
174
+ };
175
+ var useFrappeMutation = (method, httpMethod) => {
176
+ const { call: frappeCall } = useContext2(FrappeContext);
177
+ const queryKey = ["frappeCall", httpMethod, method];
178
+ const { mutateAsync, data, isPending, error, isSuccess, reset } = useMutation2({
179
+ mutationKey: queryKey,
180
+ mutationFn: (params) => {
181
+ if (httpMethod === "POST")
182
+ return frappeCall.post(method, params);
183
+ if (httpMethod === "PUT")
184
+ return frappeCall.put(method, params);
185
+ return frappeCall.delete(method, params);
186
+ }
187
+ });
188
+ return {
189
+ call: (params) => mutateAsync(params),
190
+ result: data ?? null,
191
+ loading: isPending,
192
+ error,
193
+ isCompleted: isSuccess,
194
+ reset
195
+ };
196
+ };
197
+ var useFrappePostCall = (method) => useFrappeMutation(method, "POST");
198
+ var useFrappePutCall = (method) => useFrappeMutation(method, "PUT");
199
+ var useFrappeDeleteCall = (method) => useFrappeMutation(method, "DELETE");
200
+ // src/frappe/hooks/count.ts
201
+ import { useContext as useContext3 } from "react";
202
+ import { useQuery as useQuery3 } from "@tanstack/react-query";
203
+ var useFrappeGetDocCount = (doctype, filters, debug = false, options) => {
204
+ const { url, db } = useContext3(FrappeContext);
205
+ const queryKey = ["docCount", url, doctype, filters ?? [], debug];
206
+ const { data, error, isLoading, isFetching, refetch } = useQuery3({
207
+ queryKey,
208
+ queryFn: () => db.getCount(doctype, filters, debug),
209
+ enabled: !!doctype,
210
+ ...options
211
+ });
212
+ return {
213
+ data,
214
+ error,
215
+ isLoading,
216
+ isFetching,
217
+ refetch
218
+ };
219
+ };
220
+ // src/frappe/hooks/create.ts
221
+ import { useContext as useContext4 } from "react";
222
+ import { useMutation as useMutation3 } from "@tanstack/react-query";
223
+ var useFrappeCreateDoc = () => {
224
+ const { db } = useContext4(FrappeContext);
225
+ const { mutateAsync, isPending, error, isSuccess, reset } = useMutation3({
226
+ mutationFn: ({ doctype, doc }) => db.createDoc(doctype, doc)
227
+ });
228
+ const createDoc = (doctype, doc) => mutateAsync({ doctype, doc });
229
+ return {
230
+ createDoc,
231
+ loading: isPending,
232
+ error,
233
+ isCompleted: isSuccess,
234
+ reset
235
+ };
236
+ };
237
+ // src/frappe/hooks/delete.ts
238
+ import { useContext as useContext5 } from "react";
239
+ import { useMutation as useMutation4 } from "@tanstack/react-query";
240
+ var useFrappeDeleteDoc = () => {
241
+ const { db } = useContext5(FrappeContext);
242
+ const { mutateAsync, isPending, error, isSuccess, reset } = useMutation4({
243
+ mutationFn: ({ doctype, docname }) => db.deleteDoc(doctype, docname)
244
+ });
245
+ const deleteDoc = (doctype, docname) => mutateAsync({ doctype, docname });
246
+ return {
247
+ deleteDoc,
248
+ loading: isPending,
249
+ error,
250
+ isCompleted: isSuccess,
251
+ reset
252
+ };
253
+ };
254
+ // src/frappe/hooks/event.ts
255
+ import { useCallback as useCallback2, useContext as useContext6, useState as useState2, useEffect as useEffect2 } from "react";
256
+ var useFrappeEventListener = (eventName, callback) => {
257
+ const { socket } = useContext6(FrappeContext);
258
+ useEffect2(() => {
259
+ if (socket === undefined) {
260
+ console.warn("Socket is not enabled. Please enable socket in FrappeProvider.");
261
+ }
262
+ let listener = socket?.on(eventName, callback);
263
+ return () => {
264
+ listener?.off(eventName);
265
+ };
266
+ }, [eventName, callback, socket]);
267
+ };
268
+ var useFrappeDocumentEventListener = (doctype, docname, onUpdateCallback, emitOpenCloseEventsOnMount = true) => {
269
+ const { socket } = useContext6(FrappeContext);
270
+ const [viewers, setViewers] = useState2([]);
271
+ useEffect2(() => {
272
+ if (socket === undefined) {
273
+ console.warn("Socket is not enabled. Please enable socket in FrappeProvider.");
274
+ }
275
+ socket?.emit("doc_subscribe", doctype, docname);
276
+ const onReconnect = () => {
277
+ socket?.emit("doc_subscribe", doctype, docname);
278
+ };
279
+ socket?.io.on("reconnect", onReconnect);
280
+ if (emitOpenCloseEventsOnMount) {
281
+ socket?.emit("doc_open", doctype, docname);
282
+ }
283
+ return () => {
284
+ socket?.emit("doc_unsubscribe", doctype, docname);
285
+ if (emitOpenCloseEventsOnMount) {
286
+ socket?.emit("doc_close", doctype, docname);
287
+ }
288
+ socket?.io.off("reconnect", onReconnect);
289
+ };
290
+ }, [doctype, docname, emitOpenCloseEventsOnMount, socket]);
291
+ useFrappeEventListener("doc_update", onUpdateCallback);
292
+ const emitDocOpen = useCallback2(() => {
293
+ socket?.emit("doc_open", doctype, docname);
294
+ }, [doctype, docname, socket]);
295
+ const emitDocClose = useCallback2(() => {
296
+ socket?.emit("doc_close", doctype, docname);
297
+ }, [doctype, docname, socket]);
298
+ const onViewerEvent = useCallback2((data) => {
299
+ if (data.doctype === doctype && data.docname === docname) {
300
+ setViewers(data.users);
301
+ }
302
+ }, [doctype, docname]);
303
+ useFrappeEventListener("doc_viewers", onViewerEvent);
304
+ return {
305
+ viewers,
306
+ emitDocOpen,
307
+ emitDocClose
308
+ };
309
+ };
310
+ var useFrappeDocTypeEventListener = (doctype, onListUpdateCallback) => {
311
+ const { socket } = useContext6(FrappeContext);
312
+ useEffect2(() => {
313
+ if (socket === undefined) {
314
+ console.warn("Socket is not enabled. Please enable socket in FrappeProvider.");
315
+ }
316
+ socket?.emit("doctype_subscribe", doctype);
317
+ const onReconnect = () => {
318
+ socket?.emit("doctype_subscribe", doctype);
319
+ };
320
+ socket?.io.on("reconnect", onReconnect);
321
+ return () => {
322
+ socket?.emit("doctype_unsubscribe", doctype);
323
+ socket?.io.off("reconnect", onReconnect);
324
+ };
325
+ }, [doctype, socket]);
326
+ useFrappeEventListener("list_update", onListUpdateCallback);
327
+ };
328
+ // src/frappe/hooks/file.ts
329
+ import { useContext as useContext7, useState as useState3 } from "react";
330
+ import { useMutation as useMutation5 } from "@tanstack/react-query";
331
+ var useFrappeFileUpload = () => {
332
+ const { file } = useContext7(FrappeContext);
333
+ const [progress, setProgress] = useState3(0);
334
+ const {
335
+ mutateAsync,
336
+ isPending,
337
+ error,
338
+ isSuccess,
339
+ reset: resetMutation
340
+ } = useMutation5({
341
+ mutationFn: ({ f, args, apiPath }) => {
342
+ setProgress(0);
343
+ return file.uploadFile(f, args, (completed, total) => {
344
+ if (total)
345
+ setProgress(Math.round(completed / total * 100));
346
+ }, apiPath).then((r) => {
347
+ setProgress(100);
348
+ return r.data.message;
349
+ });
350
+ }
351
+ });
352
+ const upload = (f, args, apiPath) => mutateAsync({ f, args, apiPath });
353
+ const reset = () => {
354
+ setProgress(0);
355
+ resetMutation();
356
+ };
357
+ return {
358
+ upload,
359
+ progress,
360
+ loading: isPending,
361
+ error: error ?? null,
362
+ isCompleted: isSuccess,
363
+ reset
364
+ };
365
+ };
366
+ // src/frappe/hooks/get.ts
367
+ import { useContext as useContext8 } from "react";
368
+ import { useQuery as useQuery4 } from "@tanstack/react-query";
369
+ var useFrappeGetDoc = (doctype, name, options) => {
370
+ const { url, db } = useContext8(FrappeContext);
371
+ const queryKey = ["doc", url, doctype, name];
372
+ const { data, error, isLoading, isFetching, refetch } = useQuery4({
373
+ queryKey,
374
+ queryFn: () => db.getDoc(doctype, name),
375
+ enabled: !!name,
376
+ staleTime: options?.staleTime ?? 5 * 60 * 1000,
377
+ retry: options?.retry ?? false
378
+ });
379
+ return {
380
+ data,
381
+ error,
382
+ isLoading,
383
+ isFetching,
384
+ refetch
385
+ };
386
+ };
387
+ // src/frappe/hooks/list.ts
388
+ import { useContext as useContext9 } from "react";
389
+ import {
390
+ useQuery as useQuery5
391
+ } from "@tanstack/react-query";
392
+ var useFrappeGetDocList = (doctype, args, options) => {
393
+ const { url, db } = useContext9(FrappeContext);
394
+ const queryKey = ["docList", url, doctype, args ?? null];
395
+ const { data, error, isLoading, isFetching, refetch } = useQuery5({
396
+ queryKey,
397
+ queryFn: () => db.getDocList(doctype, args),
398
+ ...options
399
+ });
400
+ return {
401
+ data,
402
+ error: error ?? null,
403
+ isLoading,
404
+ isFetching,
405
+ refetch
406
+ };
407
+ };
408
+ // src/frappe/hooks/prefetch.ts
409
+ import { useCallback as useCallback3, useContext as useContext10 } from "react";
410
+ import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
411
+ var useFrappePrefetchDoc = (doctype, name, options) => {
412
+ const queryClient = useQueryClient2();
413
+ const { url, db } = useContext10(FrappeContext);
414
+ const preloadDoc = useCallback3(() => {
415
+ if (!name)
416
+ return;
417
+ const queryKey = ["doc", url, doctype, name];
418
+ queryClient.prefetchQuery({
419
+ queryKey,
420
+ queryFn: () => db.getDoc(doctype, name),
421
+ staleTime: options?.staleTime ?? 5 * 60 * 1000
422
+ });
423
+ }, [doctype, name, queryClient, options?.staleTime]);
424
+ return preloadDoc;
425
+ };
426
+ // src/frappe/hooks/search.ts
427
+ import { useEffect as useEffect3, useState as useState4 } from "react";
428
+ var useSearch = (doctype, text, filters = [], limit = 20, debounce = 250) => {
429
+ const debouncedText = useDebounce(text, debounce);
430
+ return useFrappeGetCall("frappe.desk.search.search_link", {
431
+ doctype,
432
+ page_length: limit,
433
+ txt: debouncedText,
434
+ filters: JSON.stringify(filters ?? [])
435
+ });
436
+ };
437
+ var useDebounce = (value, delay) => {
438
+ const [debouncedValue, setDebouncedValue] = useState4(value);
439
+ useEffect3(() => {
440
+ const handler = setTimeout(() => {
441
+ setDebouncedValue(value);
442
+ }, delay);
443
+ return () => {
444
+ clearTimeout(handler);
445
+ };
446
+ }, [value, delay]);
447
+ return debouncedValue;
448
+ };
449
+ // src/frappe/hooks/update.ts
450
+ import { useContext as useContext11 } from "react";
451
+ import { useMutation as useMutation6 } from "@tanstack/react-query";
452
+ var useFrappeUpdateDoc = () => {
453
+ const { db } = useContext11(FrappeContext);
454
+ const { mutateAsync, isPending, error, isSuccess, reset } = useMutation6({
455
+ mutationFn: ({ doctype, docname, doc }) => db.updateDoc(doctype, docname, doc)
456
+ });
457
+ const updateDoc = (doctype, docname, doc) => mutateAsync({ doctype, docname, doc });
458
+ return {
459
+ updateDoc,
460
+ loading: isPending,
461
+ error,
462
+ isCompleted: isSuccess,
463
+ reset
464
+ };
465
+ };
466
+ // src/utils/char.ts
467
+ import { camelize, decamelize, camelizeKeys, decamelizeKeys } from "humps";
468
+ function equalsIgnoreCase(str1, str2) {
469
+ return str1.localeCompare(str2, undefined, { sensitivity: "accent" }) === 0;
470
+ }
471
+ export {
472
+ useSearch,
473
+ useFrappeUpdateDoc,
474
+ useFrappePutCall,
475
+ useFrappePrefetchDoc,
476
+ useFrappePostCall,
477
+ useFrappeGetDocList,
478
+ useFrappeGetDocCount,
479
+ useFrappeGetDoc,
480
+ useFrappeGetCall,
481
+ useFrappeFileUpload,
482
+ useFrappeEventListener,
483
+ useFrappeDocumentEventListener,
484
+ useFrappeDocTypeEventListener,
485
+ useFrappeDeleteDoc,
486
+ useFrappeDeleteCall,
487
+ useFrappeCreateDoc,
488
+ useFrappeAuth,
489
+ equalsIgnoreCase,
490
+ decamelizeKeys,
491
+ decamelize,
492
+ camelizeKeys,
493
+ camelize,
494
+ FrappeProvider,
495
+ FrappeContext
496
+ };
package/package.json CHANGED
@@ -1,50 +1,49 @@
1
1
  {
2
2
  "name": "@lasterp/shared",
3
- "version": "1.0.0-alpha.2",
4
- "type": "module",
5
- "description": "Shared types and business logic for LastERP Web and Native apps",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js",
13
- "require": "./dist/index.cjs"
14
- }
15
- },
3
+ "version": "1.0.0-alpha.4",
4
+ "description": "Shared repo for webapp and native app",
5
+ "license": "MIT",
16
6
  "files": [
17
- "dist",
18
- "src"
7
+ "dist"
19
8
  ],
20
9
  "scripts": {
21
- "build": "tsup",
22
- "dev": "tsup --watch --onSuccess \"yalc push\"",
23
- "type-check": "tsc --noEmit",
24
- "dev:publish": "pnpm run build && yalc publish",
25
- "dev:push": "pnpm run build && yalc push"
26
- },
27
- "keywords": [
28
- "lasterp",
29
- "shared",
30
- "typescript",
31
- "business-logic"
32
- ],
33
- "author": "",
34
- "license": "ISC",
35
- "packageManager": "pnpm@10.28.1",
36
- "peerDependencies": {
37
- "@tanstack/react-query": "^5.0.0",
38
- "react": "^18.0.0 || ^19.0.0"
10
+ "build": "bunup",
11
+ "dev": "bunup --watch",
12
+ "type-check": "tsc --noEmit"
39
13
  },
40
14
  "devDependencies": {
15
+ "@types/bun": "^1.3.9",
41
16
  "@types/humps": "^2.0.6",
42
- "@types/react": "^19",
43
- "tsup": "^8.5.1",
44
- "typescript": "^5.9.3",
45
- "yalc": "1.0.0-pre.53"
17
+ "@types/react": "^19.2.14",
18
+ "@types/react-dom": "^19.2.3",
19
+ "bunup": "^0.16.26",
20
+ "typescript": "^5.9.3"
46
21
  },
22
+ "peerDependencies": {
23
+ "typescript": ">=4.5.0"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "typescript": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "node": "./dist/node/index.js",
34
+ "rn": "./dist/rn/index.js",
35
+ "default": "./dist/node/index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "module": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
47
41
  "dependencies": {
48
- "humps": "^2.0.1"
42
+ "@tanstack/react-query": "^5.90.21",
43
+ "frappe-js-sdk": "^1.12.0",
44
+ "humps": "^2.0.1",
45
+ "react": "^19.2.4",
46
+ "react-dom": "^19.2.4",
47
+ "socket.io-client": "^4.8.3"
49
48
  }
50
49
  }