@lasterp/shared 1.0.0-alpha.0

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,261 @@
1
+ import { camelizeKeys, decamelizeKeys } from 'humps';
2
+ import type { FrappeResponse, FrappeError, AuthOptions } from './types';
3
+
4
+ async function request<T = any>(
5
+ baseUrl: string,
6
+ endpoint: string,
7
+ options: RequestInit = {},
8
+ auth?: AuthOptions
9
+ ): Promise<T> {
10
+ const url = `${baseUrl}${endpoint}`;
11
+
12
+ const headers: Record<string, string> = {
13
+ 'Content-Type': 'application/json',
14
+ Accept: 'application/json',
15
+ ...(options.headers as Record<string, string>),
16
+ };
17
+
18
+ if (auth?.token) {
19
+ headers['Authorization'] = `Bearer ${auth.token}`;
20
+ } else if (auth?.apiKey) {
21
+ headers['Authorization'] = `token ${auth.apiKey.key}:${auth.apiKey.secret}`;
22
+ }
23
+
24
+ const response = await fetch(url, {
25
+ ...options,
26
+ headers,
27
+ credentials: 'include',
28
+ });
29
+
30
+ const rawData: FrappeResponse<any> = await response.json();
31
+
32
+ if (!response.ok || rawData.exc) {
33
+ // Create a proper Error object for better serialization in Next.js
34
+ const errorMessage = rawData._error_message || rawData.exc || 'Request failed';
35
+ const error = new Error(errorMessage) as Error & FrappeError;
36
+
37
+ error.message = errorMessage;
38
+ error.statusCode = response.status;
39
+
40
+ if (rawData.exc) {
41
+ error.exc = rawData.exc;
42
+ }
43
+
44
+ if (rawData._server_messages) {
45
+ error.serverMessages = rawData._server_messages;
46
+ }
47
+
48
+ if (rawData.exc_type) {
49
+ error.excType = rawData.exc_type;
50
+ }
51
+
52
+ throw error;
53
+ }
54
+
55
+ const result = rawData.message ?? rawData;
56
+ return camelizeKeys(result) as T;
57
+ }
58
+
59
+ export async function frappeCall<T = any>(
60
+ baseUrl: string,
61
+ method: string,
62
+ args?: Record<string, any>,
63
+ auth?: AuthOptions
64
+ ): Promise<T> {
65
+ const snakeArgs = decamelizeKeys(args ?? {});
66
+
67
+ return request<T>(
68
+ baseUrl,
69
+ `/api/method/${method}`,
70
+ {
71
+ method: 'POST',
72
+ body: JSON.stringify(snakeArgs),
73
+ },
74
+ auth
75
+ );
76
+ }
77
+
78
+ export async function frappeCallGet<T = any>(
79
+ baseUrl: string,
80
+ method: string,
81
+ args?: Record<string, any>,
82
+ auth?: AuthOptions
83
+ ): Promise<T> {
84
+ const snakeArgs = decamelizeKeys(args ?? {});
85
+ const queryParams = new URLSearchParams();
86
+
87
+ Object.entries(snakeArgs).forEach(([key, value]) => {
88
+ queryParams.append(
89
+ key,
90
+ typeof value === 'string' ? value : JSON.stringify(value)
91
+ );
92
+ });
93
+
94
+ const endpoint = `/api/method/${method}?${queryParams.toString()}`;
95
+ return request<T>(baseUrl, endpoint, { method: 'GET' }, auth);
96
+ }
97
+
98
+ export async function getList<T = any>(
99
+ baseUrl: string,
100
+ params: {
101
+ doctype: string;
102
+ fields?: string[];
103
+ filters?: Record<string, any>;
104
+ orderBy?: string;
105
+ limitStart?: number;
106
+ limitPageLength?: number;
107
+ },
108
+ auth?: AuthOptions
109
+ ): Promise<T[]> {
110
+ const {
111
+ doctype,
112
+ fields = ['name'],
113
+ filters = {},
114
+ orderBy = 'modified desc',
115
+ limitStart = 0,
116
+ limitPageLength = 20,
117
+ } = params;
118
+
119
+ const snakeFilters = decamelizeKeys(filters);
120
+
121
+ const queryParams = new URLSearchParams({
122
+ fields: JSON.stringify(fields),
123
+ filters: JSON.stringify(snakeFilters),
124
+ order_by: orderBy,
125
+ limit_start: limitStart.toString(),
126
+ limit_page_length: limitPageLength.toString(),
127
+ });
128
+
129
+ const endpoint = `/api/resource/${doctype}?${queryParams.toString()}`;
130
+ const response = await request<{ data: T[] }>(baseUrl, endpoint, {}, auth);
131
+ return response.data || [];
132
+ }
133
+
134
+ export async function getDoc<T = any>(
135
+ baseUrl: string,
136
+ doctype: string,
137
+ name: string,
138
+ auth?: AuthOptions
139
+ ): Promise<T> {
140
+ const endpoint = `/api/resource/${doctype}/${encodeURIComponent(name)}`;
141
+ const response = await request<{ data: T }>(baseUrl, endpoint, {}, auth);
142
+ return response.data;
143
+ }
144
+
145
+ export async function createDoc<T = any>(
146
+ baseUrl: string,
147
+ doctype: string,
148
+ doc: Record<string, any>,
149
+ auth?: AuthOptions
150
+ ): Promise<T> {
151
+ const snakeDoc = decamelizeKeys(doc);
152
+ const endpoint = `/api/resource/${doctype}`;
153
+ const response = await request<{ data: T }>(
154
+ baseUrl,
155
+ endpoint,
156
+ {
157
+ method: 'POST',
158
+ body: JSON.stringify(snakeDoc),
159
+ },
160
+ auth
161
+ );
162
+ return response.data;
163
+ }
164
+
165
+ export async function updateDoc<T = any>(
166
+ baseUrl: string,
167
+ doctype: string,
168
+ name: string,
169
+ doc: Record<string, any>,
170
+ auth?: AuthOptions
171
+ ): Promise<T> {
172
+ const snakeDoc = decamelizeKeys(doc);
173
+ const endpoint = `/api/resource/${doctype}/${encodeURIComponent(name)}`;
174
+ const response = await request<{ data: T }>(
175
+ baseUrl,
176
+ endpoint,
177
+ {
178
+ method: 'PUT',
179
+ body: JSON.stringify(snakeDoc),
180
+ },
181
+ auth
182
+ );
183
+ return response.data;
184
+ }
185
+
186
+ export async function deleteDoc(
187
+ baseUrl: string,
188
+ doctype: string,
189
+ name: string,
190
+ auth?: AuthOptions
191
+ ): Promise<void> {
192
+ const endpoint = `/api/resource/${doctype}/${encodeURIComponent(name)}`;
193
+ await request(
194
+ baseUrl,
195
+ endpoint,
196
+ {
197
+ method: 'DELETE',
198
+ },
199
+ auth
200
+ );
201
+ }
202
+
203
+ export async function getValue<T = any>(
204
+ baseUrl: string,
205
+ doctype: string,
206
+ name: string,
207
+ fieldname: string,
208
+ auth?: AuthOptions
209
+ ): Promise<T> {
210
+ const endpoint = `/api/resource/${doctype}/${encodeURIComponent(name)}?fields=["${fieldname}"]`;
211
+ const response = await request<{ data: Record<string, T> }>(
212
+ baseUrl,
213
+ endpoint,
214
+ {},
215
+ auth
216
+ );
217
+ const value = response.data[fieldname];
218
+
219
+ if (value === undefined) {
220
+ throw new Error(
221
+ `Field "${fieldname}" not found in document "${doctype}/${name}"`
222
+ );
223
+ }
224
+
225
+ return value;
226
+ }
227
+
228
+ export async function setValue<T = any>(
229
+ baseUrl: string,
230
+ doctype: string,
231
+ name: string,
232
+ fieldname: string,
233
+ value: any,
234
+ auth?: AuthOptions
235
+ ): Promise<T> {
236
+ return updateDoc<T>(baseUrl, doctype, name, { [fieldname]: value }, auth);
237
+ }
238
+
239
+ export async function getCount(
240
+ baseUrl: string,
241
+ doctype: string,
242
+ filters: Record<string, any> = {},
243
+ auth?: AuthOptions
244
+ ): Promise<number> {
245
+ return frappeCall<number>(
246
+ baseUrl,
247
+ 'frappe.client.get_count',
248
+ {
249
+ doctype,
250
+ filters,
251
+ },
252
+ auth
253
+ );
254
+ }
255
+
256
+ export async function getCurrentUser(
257
+ baseUrl: string,
258
+ auth?: AuthOptions
259
+ ): Promise<any> {
260
+ return frappeCallGet(baseUrl, 'frappe.auth.get_logged_user', {}, auth);
261
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { useFrappeConfig } from './context';
4
+ import {
5
+ frappeCall as frappeCallCore,
6
+ frappeCallGet as frappeCallGetCore,
7
+ getDoc as getDocCore,
8
+ getList as getListCore,
9
+ createDoc as createDocCore,
10
+ updateDoc as updateDocCore,
11
+ deleteDoc as deleteDocCore,
12
+ } from './core';
13
+ import type { AuthOptions } from './types';
14
+
15
+ export function useFrappe() {
16
+ const { baseUrl, token, apiKey } = useFrappeConfig();
17
+
18
+ const auth: AuthOptions = {
19
+ ...(token && { token }),
20
+ ...(apiKey && { apiKey }),
21
+ };
22
+
23
+ return {
24
+ call: <T = any>(method: string, args?: Record<string, any>) =>
25
+ frappeCallCore<T>(baseUrl, method, args, auth),
26
+
27
+ callGet: <T = any>(method: string, args?: Record<string, any>) =>
28
+ frappeCallGetCore<T>(baseUrl, method, args, auth),
29
+
30
+ getList: <T = any>(params: {
31
+ doctype: string;
32
+ fields?: string[];
33
+ filters?: Record<string, any>;
34
+ orderBy?: string;
35
+ limitStart?: number;
36
+ limitPageLength?: number;
37
+ }) => getListCore<T>(baseUrl, params, auth),
38
+
39
+ getDoc: <T = any>(doctype: string, name: string) =>
40
+ getDocCore<T>(baseUrl, doctype, name, auth),
41
+
42
+ createDoc: <T = any>(doctype: string, doc: Record<string, any>) =>
43
+ createDocCore<T>(baseUrl, doctype, doc, auth),
44
+
45
+ updateDoc: <T = any>(
46
+ doctype: string,
47
+ name: string,
48
+ doc: Record<string, any>
49
+ ) => updateDocCore<T>(baseUrl, doctype, name, doc, auth),
50
+
51
+ deleteDoc: (doctype: string, name: string) =>
52
+ deleteDocCore(baseUrl, doctype, name, auth),
53
+
54
+ baseUrl,
55
+ auth,
56
+ };
57
+ }
@@ -0,0 +1,21 @@
1
+ export {
2
+ frappeCall,
3
+ frappeCallGet,
4
+ getList,
5
+ getDoc,
6
+ createDoc,
7
+ updateDoc,
8
+ deleteDoc,
9
+ getValue,
10
+ setValue,
11
+ getCount,
12
+ getCurrentUser,
13
+ } from './core';
14
+
15
+ export type { FrappeResponse, FrappeError, AuthOptions } from './types';
16
+
17
+ export { FrappeProvider, useFrappeConfig } from './context';
18
+ export type { FrappeContextValue, FrappeProviderProps } from './context';
19
+ export { useFrappe } from './hooks';
20
+
21
+ export * from './storage';
@@ -0,0 +1,76 @@
1
+ export interface StorageAdapter {
2
+ getItem(key: string): string | null | Promise<string | null>;
3
+ setItem(key: string, value: string): void | Promise<void>;
4
+ removeItem(key: string): void | Promise<void>;
5
+ }
6
+
7
+ export const webStorageAdapter: StorageAdapter = {
8
+ getItem: (key: string) => {
9
+ if (typeof window === 'undefined') return null;
10
+ return localStorage.getItem(key);
11
+ },
12
+ setItem: (key: string, value: string) => {
13
+ if (typeof window === 'undefined') return;
14
+ localStorage.setItem(key, value);
15
+ },
16
+ removeItem: (key: string) => {
17
+ if (typeof window === 'undefined') return;
18
+ localStorage.removeItem(key);
19
+ },
20
+ };
21
+
22
+ let currentAdapter: StorageAdapter = webStorageAdapter;
23
+
24
+ export function setStorageAdapter(adapter: StorageAdapter) {
25
+ currentAdapter = adapter;
26
+ }
27
+
28
+ export function getStorageAdapter(): StorageAdapter {
29
+ return currentAdapter;
30
+ }
31
+
32
+ const TOKEN_KEY = 'frappe_token';
33
+ const API_KEY_KEY = 'frappe_api_key';
34
+ const API_SECRET_KEY = 'frappe_api_secret';
35
+
36
+ export function getToken(): string | null | Promise<string | null> {
37
+ return currentAdapter.getItem(TOKEN_KEY);
38
+ }
39
+
40
+ export function setToken(token: string): void | Promise<void> {
41
+ return currentAdapter.setItem(TOKEN_KEY, token);
42
+ }
43
+
44
+ export function clearToken(): void | Promise<void> {
45
+ return currentAdapter.removeItem(TOKEN_KEY);
46
+ }
47
+
48
+ export async function getApiKey(): Promise<{ key: string; secret: string } | null> {
49
+ const key = await currentAdapter.getItem(API_KEY_KEY);
50
+ const secret = await currentAdapter.getItem(API_SECRET_KEY);
51
+
52
+ if (!key || !secret) return null;
53
+
54
+ return { key, secret };
55
+ }
56
+
57
+ export async function setApiKey(key: string, secret: string): Promise<void> {
58
+ await currentAdapter.setItem(API_KEY_KEY, key);
59
+ await currentAdapter.setItem(API_SECRET_KEY, secret);
60
+ }
61
+
62
+ export async function clearApiKey(): Promise<void> {
63
+ await currentAdapter.removeItem(API_KEY_KEY);
64
+ await currentAdapter.removeItem(API_SECRET_KEY);
65
+ }
66
+
67
+ export async function clearAuth(): Promise<void> {
68
+ await clearToken();
69
+ await clearApiKey();
70
+ }
71
+
72
+ export async function isAuthenticated(): Promise<boolean> {
73
+ const token = await getToken();
74
+ const apiKey = await getApiKey();
75
+ return !!token || !!apiKey;
76
+ }
@@ -0,0 +1,24 @@
1
+ export interface FrappeResponse<T = any> {
2
+ message?: T;
3
+ docs?: T[];
4
+ exc?: string;
5
+ exc_type?: string;
6
+ _server_messages?: string;
7
+ _error_message?: string;
8
+ }
9
+
10
+ export interface FrappeError {
11
+ message: string;
12
+ statusCode?: number;
13
+ exc?: string;
14
+ excType?: string;
15
+ serverMessages?: string;
16
+ }
17
+
18
+ export interface AuthOptions {
19
+ token?: string;
20
+ apiKey?: {
21
+ key: string;
22
+ secret: string;
23
+ };
24
+ }
@@ -0,0 +1 @@
1
+ export type { FrappeDoc, FrappeChildDoc } from './types';
@@ -0,0 +1,15 @@
1
+ export interface FrappeDoc {
2
+ name: string;
3
+ creation?: string;
4
+ modified?: string;
5
+ modifiedBy?: string;
6
+ owner?: string;
7
+ docStatus?: number;
8
+ idx?: number;
9
+ }
10
+
11
+ export interface FrappeChildDoc extends FrappeDoc {
12
+ parent: string;
13
+ parentType: string;
14
+ parentField: string;
15
+ }
@@ -0,0 +1,8 @@
1
+ export interface AaveFeatureSlidesData {
2
+ feature1Title: string
3
+ feature1Description: string
4
+ feature1Cta?: string
5
+ feature2Title: string
6
+ feature2Description: string
7
+ feature2Cta?: string
8
+ }
@@ -0,0 +1,20 @@
1
+
2
+ export interface FooterItemGroup {
3
+ title: string;
4
+ items: FooterItem[];
5
+ }
6
+
7
+ export interface FooterItem {
8
+ label: string;
9
+ link?: string;
10
+ }
11
+
12
+ export interface Footer {
13
+ footerType?: string;
14
+ groups: FooterItemGroup[];
15
+ copyright?: string;
16
+ address?: string;
17
+ country?: string;
18
+ phone?: string;
19
+ email?: string;
20
+ }
@@ -0,0 +1,43 @@
1
+ export interface Brand {
2
+ brandImage?: string;
3
+ }
4
+
5
+ export interface NavbarItem {
6
+ label: string;
7
+ enableDropdown: boolean;
8
+ enableLink: boolean;
9
+ link?: string;
10
+ dropdownDescription?: string;
11
+ dropdownCta?: string;
12
+ groups?: NavbarSubItemGroup[]
13
+ }
14
+
15
+ export interface NavbarSubItemGroup {
16
+ title?: string;
17
+ items: NavbarSubItem[];
18
+ }
19
+
20
+ export interface NavbarSubItem {
21
+ label: string;
22
+ description?: string;
23
+ image?: string;
24
+ link?: string;
25
+ }
26
+
27
+ export interface Topbar {
28
+ topbarEnabled?: boolean;
29
+ items: TopbarItem[];
30
+ }
31
+
32
+ export interface TopbarItem {
33
+ icon?: string;
34
+ label: string;
35
+ link?: string;
36
+ }
37
+
38
+ export interface Header {
39
+ brand: Brand;
40
+ headerType?: string;
41
+ tabs: NavbarItem[];
42
+ topbar: Topbar;
43
+ }
@@ -0,0 +1,7 @@
1
+ import type {Header} from "./header";
2
+ import type {Footer} from "./footer";
3
+
4
+ export interface Globals {
5
+ header: Header
6
+ footer: Footer
7
+ }
@@ -0,0 +1,8 @@
1
+ export * from './globals/types';
2
+ export * from './globals/header';
3
+ export * from './globals/footer';
4
+
5
+ export * from './page/types';
6
+ export * from './page/api';
7
+
8
+ export * from './block/types';
@@ -0,0 +1,11 @@
1
+ import { getDoc } from '../../client'
2
+ import type { AuthOptions } from '../../client'
3
+ import type { Page } from './types'
4
+
5
+ export async function getPage(
6
+ baseUrl: string,
7
+ slug: string,
8
+ auth?: AuthOptions
9
+ ): Promise<Page> {
10
+ return getDoc<Page>(baseUrl, 'Design Page', slug, auth)
11
+ }
@@ -0,0 +1,15 @@
1
+ export interface Hero {
2
+ type: string;
3
+ data: Record<string, unknown>;
4
+ }
5
+
6
+ export interface Block {
7
+ type: string;
8
+ data?: Record<string, unknown>;
9
+ }
10
+
11
+ export interface Page {
12
+ slug: string;
13
+ hero?: Hero;
14
+ blocks: Block[];
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './common'
2
+ export * from './client'
3
+ export * from './design'
4
+ export * from './utils'
5
+ export * from './lasterp'
@@ -0,0 +1,24 @@
1
+ export interface Item {
2
+ itemCode: string
3
+ region: string
4
+ grade: string
5
+ gradeIssuer: string
6
+ color: string
7
+ storage: string
8
+ memory: string
9
+ network: string
10
+ }
11
+
12
+ export interface ItemVariant extends Item {
13
+ itemVariant: string
14
+ }
15
+
16
+ export interface ModelNumber {
17
+ modelNumber: string
18
+ simCardType: string
19
+ }
20
+
21
+ export interface Colour {
22
+ name: string
23
+ color: string
24
+ }
@@ -0,0 +1,5 @@
1
+ export * from './catalog/types';
2
+
3
+ export * from './shop/types';
4
+ export * from './shop/api';
5
+ export * from './shop/hooks';
@@ -0,0 +1,32 @@
1
+ import { frappeCall } from '../../client';
2
+ import type { AuthOptions } from '../../client';
3
+ import type { ShopContext, ProductContext } from './types';
4
+
5
+ export async function getShopContext(
6
+ baseUrl: string,
7
+ params?: {
8
+ categoryName?: string;
9
+ gradeName?: string;
10
+ },
11
+ auth?: AuthOptions
12
+ ): Promise<ShopContext> {
13
+ return frappeCall<ShopContext>(
14
+ baseUrl,
15
+ 'lasterp.shop.controllers.shop_controller.get_context',
16
+ params,
17
+ auth
18
+ );
19
+ }
20
+
21
+ export async function getProductContext(
22
+ baseUrl: string,
23
+ productName: string,
24
+ auth?: AuthOptions
25
+ ): Promise<ProductContext> {
26
+ return frappeCall<ProductContext>(
27
+ baseUrl,
28
+ 'lasterp.shop.controllers.product_controller.get_context',
29
+ { productName },
30
+ auth
31
+ );
32
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { useQuery, type UseQueryOptions } from '@tanstack/react-query';
4
+ import { useFrappe } from '../../client';
5
+ import { getShopContext, getProductContext } from './api';
6
+ import type { ShopContext, ProductContext } from './types';
7
+
8
+ export function useShopContext(
9
+ params?: {
10
+ categoryName?: string;
11
+ gradeName?: string;
12
+ },
13
+ options?: Omit<
14
+ UseQueryOptions<ShopContext, Error>,
15
+ 'queryKey' | 'queryFn'
16
+ >
17
+ ) {
18
+ const { baseUrl, auth } = useFrappe();
19
+
20
+ return useQuery<ShopContext, Error>({
21
+ queryKey: ['shop-context', params],
22
+ queryFn: () => getShopContext(baseUrl, params, auth),
23
+ ...options,
24
+ });
25
+ }
26
+
27
+ export function useProductContext(
28
+ productName: string,
29
+ options?: Omit<
30
+ UseQueryOptions<ProductContext, Error>,
31
+ 'queryKey' | 'queryFn'
32
+ >
33
+ ) {
34
+ const { baseUrl, auth } = useFrappe();
35
+
36
+ return useQuery<ProductContext, Error>({
37
+ queryKey: ['product-context', productName],
38
+ queryFn: () => getProductContext(baseUrl, productName, auth),
39
+ enabled: !!productName,
40
+ ...options,
41
+ });
42
+ }