nexabase-console 1.0.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.
- package/README.md +237 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +651 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# NexaBase JavaScript SDK (`nexabase`)
|
|
2
|
+
|
|
3
|
+
SDK Client Resmi untuk **NexaBase**: Platform sinkronisasi database modern yang menyatukan fungsionalitas Firestore NoSQL, Real-Time Database, File Storage, dan Edge Authentication dengan dukungan **Offline-First (IndexedDB)** serta **Passwordless OTP**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Fitur Utama
|
|
8
|
+
|
|
9
|
+
- **⚡ Real-Time Synchronization**: Sinkronisasi data real-time dengan latency rendah menggunakan Server-Sent Events (SSE).
|
|
10
|
+
- **📦 Offline-First & Auto-Sync**: Sinkronisasi state lokal secara otomatis menggunakan **IndexedDB**. Data tetap tersimpan di browser jika koneksi terputus dan akan disinkronkan ke server secara otomatis saat kembali online.
|
|
11
|
+
- **🔥 Modular Firestore API**: Desain API modular modern mirip Firebase SDK (v9+) sehingga sangat mudah dipelajari.
|
|
12
|
+
- **🔑 Secure Auth & Passwordless OTP**: Dukungan penuh registrasi, login email/sandi, serta OTP instan via SMTP yang aman.
|
|
13
|
+
- **📂 File Storage Integration**: Unggah aset media dan file blob ke cloud storage dengan satu perintah.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📦 Instalasi
|
|
18
|
+
|
|
19
|
+
Instal package melalui npm atau yarn:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install nexabase
|
|
23
|
+
# atau
|
|
24
|
+
yarn add nexabase
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚡ Panduan Rilis (Publishing)
|
|
30
|
+
|
|
31
|
+
Jika Anda ingin mengunggah SDK ini ke repositori **GitHub** Anda sendiri atau merilisnya ke registry **NPM**, ikuti langkah mudah berikut:
|
|
32
|
+
|
|
33
|
+
### 1. Inisialisasi Repositori Git & GitHub
|
|
34
|
+
```bash
|
|
35
|
+
# Masuk ke direktori SDK
|
|
36
|
+
cd nexabase-sdk
|
|
37
|
+
|
|
38
|
+
# Inisialisasi Git
|
|
39
|
+
git init
|
|
40
|
+
git add .
|
|
41
|
+
git commit -m "Inisialisasi rilis perdana NexaBase SDK v1.0.0"
|
|
42
|
+
|
|
43
|
+
# Tambahkan remote repositori GitHub Anda
|
|
44
|
+
git remote add origin https://github.com/USERNAME/REPOS_NAME.git
|
|
45
|
+
git branch -M main
|
|
46
|
+
git push -u origin main
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Publikasi ke NPM Registry
|
|
50
|
+
Pastikan Anda sudah memiliki akun di [npmjs.com](https://www.npmjs.com/).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Login ke akun NPM Anda melalui terminal
|
|
54
|
+
npm login
|
|
55
|
+
|
|
56
|
+
# Jalankan build compiler TypeScript
|
|
57
|
+
npm run build
|
|
58
|
+
|
|
59
|
+
# Publikasikan ke registry NPM secara publik
|
|
60
|
+
npm publish --access public
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🛠️ Cara Penggunaan & Contoh Kode
|
|
66
|
+
|
|
67
|
+
### 1. Inisialisasi Aplikasi
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
import { initializeApp, getFirestore } from 'nexabase';
|
|
71
|
+
|
|
72
|
+
const app = initializeApp({
|
|
73
|
+
projectId: 'PROYEK_NEXABASE_ID_ANDA',
|
|
74
|
+
apiKey: 'NEXABASE_API_KEY_ANDA', // Opsional, tergantung aturan keamanan
|
|
75
|
+
endpoint: 'https://nexabase-server.yourdomain.com' // Alamat URL server backend
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const db = getFirestore(app);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 2. Firestore-like Modular API (NoSQL)
|
|
84
|
+
|
|
85
|
+
#### Menulis Data Baru (Set Doc)
|
|
86
|
+
```javascript
|
|
87
|
+
import { doc, setDoc } from 'nexabase';
|
|
88
|
+
|
|
89
|
+
const main = async () => {
|
|
90
|
+
const profileRef = doc(db, 'users', 'budi_santoso');
|
|
91
|
+
const res = await setDoc(profileRef, {
|
|
92
|
+
name: 'Budi Santoso',
|
|
93
|
+
email: 'budi@example.com',
|
|
94
|
+
age: 28,
|
|
95
|
+
hobbies: ['Coding', 'Cycling']
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log("Data berhasil disimpan:", res);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
main();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Memperbarui Sebagian Data (Update Doc)
|
|
105
|
+
```javascript
|
|
106
|
+
import { doc, updateDoc } from 'nexabase';
|
|
107
|
+
|
|
108
|
+
const editData = async () => {
|
|
109
|
+
const profileRef = doc(db, 'users', 'budi_santoso');
|
|
110
|
+
await updateDoc(profileRef, {
|
|
111
|
+
age: 29 // Hanya memperbarui field age
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Membaca Satu Dokumen (Get Doc)
|
|
117
|
+
```javascript
|
|
118
|
+
import { doc, getDoc } from 'nexabase';
|
|
119
|
+
|
|
120
|
+
const readData = async () => {
|
|
121
|
+
const profileRef = doc(db, 'users', 'budi_santoso');
|
|
122
|
+
const snapshot = await getDoc(profileRef);
|
|
123
|
+
|
|
124
|
+
if (snapshot.exists()) {
|
|
125
|
+
console.log("Isi data:", snapshot.data());
|
|
126
|
+
} else {
|
|
127
|
+
console.log("Dokumen tidak ditemukan.");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Mendengarkan Perubahan Data secara Real-Time (onSnapshot)
|
|
133
|
+
```javascript
|
|
134
|
+
import { doc, onSnapshot } from 'nexabase';
|
|
135
|
+
|
|
136
|
+
const listenLive = () => {
|
|
137
|
+
const docRef = doc(db, 'chats', 'global_room');
|
|
138
|
+
|
|
139
|
+
// onSnapshot akan terpanggil seketika jika ada perubahan data baik di lokal maupun di server
|
|
140
|
+
const unsubscribe = onSnapshot(docRef, (snapshot) => {
|
|
141
|
+
console.log("⚡ Perubahan Terdeteksi! Data Terbaru:", snapshot.data());
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Panggil unsubscribe() untuk menghentikan pemantauan realtime
|
|
145
|
+
// unsubscribe();
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Menggunakan Atomic Write Batch
|
|
150
|
+
```javascript
|
|
151
|
+
import { doc, writeBatch } from 'nexabase';
|
|
152
|
+
|
|
153
|
+
const executeBatch = async () => {
|
|
154
|
+
const batch = writeBatch(db);
|
|
155
|
+
|
|
156
|
+
const ref1 = doc(db, 'products', 'prod_a');
|
|
157
|
+
const ref2 = doc(db, 'products', 'prod_b');
|
|
158
|
+
|
|
159
|
+
batch.set(ref1, { name: 'Sandal Kulit', price: 75000 });
|
|
160
|
+
batch.set(ref2, { name: 'Sepatu Kulit', price: 250000 });
|
|
161
|
+
|
|
162
|
+
const result = await batch.commit();
|
|
163
|
+
console.log("Semua operasi batch berhasil di-commit secara atomik.");
|
|
164
|
+
};
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### 3. Autentikasi Pengguna & OTP Tanpa Sandi
|
|
170
|
+
|
|
171
|
+
#### Pendaftaran & Login Tradisional
|
|
172
|
+
```javascript
|
|
173
|
+
// Registrasi User Baru
|
|
174
|
+
const register = async () => {
|
|
175
|
+
const user = await app.auth().createUserWithEmailAndPassword(
|
|
176
|
+
'test@example.com',
|
|
177
|
+
'password123',
|
|
178
|
+
'Andi Wijaya'
|
|
179
|
+
);
|
|
180
|
+
console.log("Registrasi Berhasil:", user);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Login User
|
|
184
|
+
const login = async () => {
|
|
185
|
+
const authSession = await app.auth().signInWithEmailAndPassword(
|
|
186
|
+
'test@example.com',
|
|
187
|
+
'password123'
|
|
188
|
+
);
|
|
189
|
+
console.log("Token Sesi JWT:", authSession.token);
|
|
190
|
+
console.log("Data Akun Pengguna:", authSession.user);
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### Login Passwordless dengan OTP (Email)
|
|
195
|
+
```javascript
|
|
196
|
+
// 1. Kirim OTP Ke Email Pengguna
|
|
197
|
+
const sendMyOtp = async () => {
|
|
198
|
+
const res = await app.auth().sendOtp('test@example.com');
|
|
199
|
+
console.log("Kode OTP berhasil terkirim ke email Anda.");
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// 2. Verifikasi OTP dari Input Pengguna
|
|
203
|
+
const verifyMyOtp = async () => {
|
|
204
|
+
const otpCode = '123456'; // Kode 6 digit yang dikirim ke email
|
|
205
|
+
const authSession = await app.auth().signInWithOtp('test@example.com', otpCode, 'Andi Wijaya');
|
|
206
|
+
console.log("Login OTP Berhasil! Sesi JWT Aktif:", authSession.token);
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### 4. File Storage API
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
const uploadImage = async (fileBlob) => {
|
|
216
|
+
const storageRef = app.storage().ref('avatars/andi.jpg');
|
|
217
|
+
const result = await storageRef.put(fileBlob);
|
|
218
|
+
|
|
219
|
+
console.log("Akses File URL Publik Anda:", result.url);
|
|
220
|
+
};
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 🗄️ Dukungan Sinkronisasi Offline (IndexedDB)
|
|
226
|
+
|
|
227
|
+
NexaBase JS SDK dikembangkan dengan arsitektur **Offline-First**.
|
|
228
|
+
|
|
229
|
+
1. **Optimistic Rendering**: Jika perangkat offline, SDK akan langsung mengupdate cache IndexedDB lokal dan men-trigger callback subscriber `onSnapshot` agar UI pengguna langsung merespons secara instan.
|
|
230
|
+
2. **Background Queue**: Setiap perubahan mutasi (`setDoc`, `updateDoc`, `deleteDoc`, `writeBatch`) saat offline disimpan secara aman di IndexedDB.
|
|
231
|
+
3. **Automatic Resync**: Saat koneksi internet mendeteksi status `online`, antrean modifikasi akan dikirimkan kembali secara beruntun ke Server cloud NexaBase secara otomatis tanpa intervensi pengguna.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 📄 Lisensi
|
|
236
|
+
|
|
237
|
+
Proyek ini dilisensikan di bawah Lisensi MIT. Bebas digunakan untuk keperluan pribadi, komersial, maupun edukasi.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
export interface NexaConfig {
|
|
3
|
+
projectId: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface User {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string;
|
|
11
|
+
role: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AuthResponse {
|
|
14
|
+
token: string;
|
|
15
|
+
user: User;
|
|
16
|
+
}
|
|
17
|
+
export interface QueryConstraint {
|
|
18
|
+
type: 'where' | 'orderBy' | 'limit';
|
|
19
|
+
fieldPath?: string;
|
|
20
|
+
opStr?: string;
|
|
21
|
+
value?: any;
|
|
22
|
+
directionStr?: 'asc' | 'desc';
|
|
23
|
+
limitValue?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface DocumentReference {
|
|
26
|
+
type: 'document';
|
|
27
|
+
path: string;
|
|
28
|
+
db: NexaApp;
|
|
29
|
+
}
|
|
30
|
+
export interface CollectionReference {
|
|
31
|
+
type: 'collection';
|
|
32
|
+
path: string;
|
|
33
|
+
db: NexaApp;
|
|
34
|
+
}
|
|
35
|
+
export interface Query {
|
|
36
|
+
type: 'query' | 'collection';
|
|
37
|
+
path: string;
|
|
38
|
+
db: NexaApp;
|
|
39
|
+
constraints?: QueryConstraint[];
|
|
40
|
+
}
|
|
41
|
+
export interface DocumentSnapshot {
|
|
42
|
+
exists: () => boolean;
|
|
43
|
+
data: () => any | null;
|
|
44
|
+
}
|
|
45
|
+
export interface QueryDocumentSnapshot {
|
|
46
|
+
id: string;
|
|
47
|
+
data: () => any;
|
|
48
|
+
}
|
|
49
|
+
export interface QuerySnapshot {
|
|
50
|
+
docs: QueryDocumentSnapshot[];
|
|
51
|
+
forEach: (callback: (doc: QueryDocumentSnapshot) => void) => void;
|
|
52
|
+
empty: boolean;
|
|
53
|
+
size: number;
|
|
54
|
+
}
|
|
55
|
+
export interface DatabaseReference {
|
|
56
|
+
get: () => Promise<any>;
|
|
57
|
+
set: (data: any) => Promise<any>;
|
|
58
|
+
onDataChanged: (callback: (data: any) => void) => void;
|
|
59
|
+
}
|
|
60
|
+
export interface StorageReference {
|
|
61
|
+
put: (file: File | Blob) => Promise<{
|
|
62
|
+
url: string;
|
|
63
|
+
success: boolean;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
export interface WriteBatch {
|
|
67
|
+
set: (docRef: DocumentReference, data: any) => WriteBatch;
|
|
68
|
+
update: (docRef: DocumentReference, data: any) => WriteBatch;
|
|
69
|
+
delete: (docRef: DocumentReference) => WriteBatch;
|
|
70
|
+
commit: () => Promise<any>;
|
|
71
|
+
}
|
|
72
|
+
interface OfflineJob {
|
|
73
|
+
id: string;
|
|
74
|
+
type: 'set' | 'update' | 'delete';
|
|
75
|
+
path: string;
|
|
76
|
+
data?: any;
|
|
77
|
+
}
|
|
78
|
+
export declare class NexaApp {
|
|
79
|
+
projectId: string;
|
|
80
|
+
token: string | null;
|
|
81
|
+
endpoint: string;
|
|
82
|
+
client: AxiosInstance;
|
|
83
|
+
sse: EventSource | null;
|
|
84
|
+
_firestoreCache: Record<string, any>;
|
|
85
|
+
_snapshotCallbacks: Record<string, (payload?: any) => void>;
|
|
86
|
+
_sseCallbacks: {
|
|
87
|
+
data_changed: ((payload: any) => void)[];
|
|
88
|
+
firestore_changed: ((payload: any) => void)[];
|
|
89
|
+
};
|
|
90
|
+
_offlineQueueInitialized: boolean;
|
|
91
|
+
_isSyncing: boolean;
|
|
92
|
+
_idb: any;
|
|
93
|
+
_firestoreListening: boolean;
|
|
94
|
+
_initPromise: Promise<void> | null;
|
|
95
|
+
constructor(config: NexaConfig);
|
|
96
|
+
connectRealtime(): EventSource;
|
|
97
|
+
database(): {
|
|
98
|
+
ref: (path?: string) => DatabaseReference;
|
|
99
|
+
};
|
|
100
|
+
auth(): {
|
|
101
|
+
signInWithEmailAndPassword: (email: string, password: string) => Promise<AuthResponse>;
|
|
102
|
+
createUserWithEmailAndPassword: (email: string, password: string, name: string) => Promise<User>;
|
|
103
|
+
sendOtp: (email: string) => Promise<{
|
|
104
|
+
success: boolean;
|
|
105
|
+
message: string;
|
|
106
|
+
}>;
|
|
107
|
+
signInWithOtp: (email: string, otp: string, name?: string) => Promise<AuthResponse>;
|
|
108
|
+
signOut: () => void;
|
|
109
|
+
getCurrentUser: () => User | null;
|
|
110
|
+
};
|
|
111
|
+
storage(): {
|
|
112
|
+
ref: (path?: string) => StorageReference;
|
|
113
|
+
};
|
|
114
|
+
_initFirestoreState(): Promise<void>;
|
|
115
|
+
_getOfflineQueue(): Promise<OfflineJob[]>;
|
|
116
|
+
_saveOfflineQueue(queue: OfflineJob[]): Promise<void>;
|
|
117
|
+
_addOfflineJob(job: OfflineJob): Promise<void>;
|
|
118
|
+
_setCache(path: string, data: any): Promise<void>;
|
|
119
|
+
_deleteCache(path: string): Promise<void>;
|
|
120
|
+
_loadCache(): Promise<void>;
|
|
121
|
+
_syncOfflineQueue(): Promise<void>;
|
|
122
|
+
_ensureFirestoreSSE(): void;
|
|
123
|
+
}
|
|
124
|
+
export declare const initializeApp: (config: NexaConfig) => NexaApp;
|
|
125
|
+
export declare const getFirestore: (app: NexaApp) => NexaApp;
|
|
126
|
+
export declare const collection: (db: NexaApp, collectionPath: string) => CollectionReference;
|
|
127
|
+
export declare const doc: (dbOrCollection: NexaApp | CollectionReference, ...pathSegments: string[]) => DocumentReference;
|
|
128
|
+
export declare const query: (queryObject: Query | CollectionReference, ...queryConstraints: QueryConstraint[]) => Query;
|
|
129
|
+
export declare const where: (fieldPath: string, opStr: "==" | ">" | "<" | ">=" | "<=" | "!=", value: any) => QueryConstraint;
|
|
130
|
+
export declare const orderBy: (fieldPath: string, directionStr?: "asc" | "desc") => QueryConstraint;
|
|
131
|
+
export declare const limit: (limitValue: number) => QueryConstraint;
|
|
132
|
+
export declare const getDocs: (queryOrCollection: Query | CollectionReference) => Promise<QuerySnapshot>;
|
|
133
|
+
export declare const getDoc: (docRef: DocumentReference) => Promise<DocumentSnapshot>;
|
|
134
|
+
export declare const setDoc: (docRef: DocumentReference, data: any) => Promise<any>;
|
|
135
|
+
export declare const updateDoc: (docRef: DocumentReference, data: any) => Promise<any>;
|
|
136
|
+
export declare const deleteDoc: (docRef: DocumentReference) => Promise<any>;
|
|
137
|
+
export declare const onSnapshot: (ref: CollectionReference | DocumentReference, callback: (snapshot: any) => void) => (() => void);
|
|
138
|
+
export declare const writeBatch: (db: NexaApp) => WriteBatch;
|
|
139
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeBatch = exports.onSnapshot = exports.deleteDoc = exports.updateDoc = exports.setDoc = exports.getDoc = exports.getDocs = exports.limit = exports.orderBy = exports.where = exports.query = exports.doc = exports.collection = exports.getFirestore = exports.initializeApp = exports.NexaApp = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
// -----------------------------------------------------------------
|
|
9
|
+
// Main NexaApp SDK Class
|
|
10
|
+
// -----------------------------------------------------------------
|
|
11
|
+
class NexaApp {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.token = null;
|
|
14
|
+
this.sse = null;
|
|
15
|
+
// Internal Firestore and Realtime caches/states
|
|
16
|
+
this._firestoreCache = {};
|
|
17
|
+
this._snapshotCallbacks = {};
|
|
18
|
+
this._sseCallbacks = { data_changed: [], firestore_changed: [] };
|
|
19
|
+
this._offlineQueueInitialized = false;
|
|
20
|
+
this._isSyncing = false;
|
|
21
|
+
this._idb = null;
|
|
22
|
+
this._firestoreListening = false;
|
|
23
|
+
this._initPromise = null;
|
|
24
|
+
this.projectId = config.projectId;
|
|
25
|
+
this.token = config.apiKey || null;
|
|
26
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
27
|
+
const savedToken = localStorage.getItem(`nexa_token_${this.projectId}`);
|
|
28
|
+
if (savedToken) {
|
|
29
|
+
this.token = savedToken;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.endpoint = config.endpoint || (typeof window !== 'undefined' ? window.location.origin : '');
|
|
33
|
+
this.client = axios_1.default.create({
|
|
34
|
+
baseURL: this.endpoint,
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Request interceptor to automatically attach authorization Bearer token
|
|
40
|
+
this.client.interceptors.request.use((req) => {
|
|
41
|
+
if (this.token) {
|
|
42
|
+
req.headers.Authorization = `Bearer ${this.token}`;
|
|
43
|
+
}
|
|
44
|
+
return req;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Real-time listener connection via Server-Sent Events (SSE)
|
|
48
|
+
connectRealtime() {
|
|
49
|
+
if (this.sse)
|
|
50
|
+
return this.sse;
|
|
51
|
+
const url = `${this.endpoint}/api/firestore/${this.projectId}/listen`;
|
|
52
|
+
const listenUrl = this.token ? `${url}?token=${this.token}` : url;
|
|
53
|
+
this.sse = new EventSource(listenUrl);
|
|
54
|
+
this.sse.onmessage = async (event) => {
|
|
55
|
+
try {
|
|
56
|
+
const payload = JSON.parse(event.data);
|
|
57
|
+
if (payload.type === 'connected')
|
|
58
|
+
return;
|
|
59
|
+
if (payload.type && (payload.type.includes('document_') || payload.type === 'bulk_update')) {
|
|
60
|
+
this._sseCallbacks.firestore_changed.forEach((cb) => cb(payload));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this._sseCallbacks.data_changed.forEach((cb) => cb(payload));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
// Silent catch JSON parsing errors
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return this.sse;
|
|
71
|
+
}
|
|
72
|
+
// -----------------------------------------------------------------
|
|
73
|
+
// Realtime Key-Value Database API
|
|
74
|
+
// -----------------------------------------------------------------
|
|
75
|
+
database() {
|
|
76
|
+
return {
|
|
77
|
+
ref: (path = '') => ({
|
|
78
|
+
get: async () => {
|
|
79
|
+
const response = await this.client.get(`/api/db/${this.projectId}/${path}`);
|
|
80
|
+
return response.data;
|
|
81
|
+
},
|
|
82
|
+
set: async (data) => {
|
|
83
|
+
const payload = data instanceof Map ? Object.fromEntries(data) : data;
|
|
84
|
+
const response = await this.client.put(`/api/db/${this.projectId}/${path}`, payload);
|
|
85
|
+
return response.data;
|
|
86
|
+
},
|
|
87
|
+
onDataChanged: (callback) => {
|
|
88
|
+
this.connectRealtime();
|
|
89
|
+
this._sseCallbacks.data_changed.push((payload) => {
|
|
90
|
+
if (payload.path === path || payload.path === '/' + path) {
|
|
91
|
+
callback(payload.data);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// -----------------------------------------------------------------
|
|
99
|
+
// User Authentication API
|
|
100
|
+
// -----------------------------------------------------------------
|
|
101
|
+
auth() {
|
|
102
|
+
return {
|
|
103
|
+
signInWithEmailAndPassword: async (email, password) => {
|
|
104
|
+
const res = await this.client.post(`/api/project/${this.projectId}/auth/login`, { email, password });
|
|
105
|
+
if (res.data.token) {
|
|
106
|
+
this.token = res.data.token;
|
|
107
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
108
|
+
localStorage.setItem(`nexa_token_${this.projectId}`, res.data.token);
|
|
109
|
+
localStorage.setItem(`nexa_user_${this.projectId}`, JSON.stringify(res.data.user));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return res.data;
|
|
113
|
+
},
|
|
114
|
+
createUserWithEmailAndPassword: async (email, password, name) => {
|
|
115
|
+
const res = await this.client.post(`/api/project/${this.projectId}/users`, { email, password, name });
|
|
116
|
+
return res.data;
|
|
117
|
+
},
|
|
118
|
+
sendOtp: async (email) => {
|
|
119
|
+
const res = await this.client.post(`/api/project/${this.projectId}/auth/otp/send`, { email });
|
|
120
|
+
return res.data;
|
|
121
|
+
},
|
|
122
|
+
signInWithOtp: async (email, otp, name) => {
|
|
123
|
+
const res = await this.client.post(`/api/project/${this.projectId}/auth/otp/login`, { email, otp, name });
|
|
124
|
+
if (res.data.token) {
|
|
125
|
+
this.token = res.data.token;
|
|
126
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
127
|
+
localStorage.setItem(`nexa_token_${this.projectId}`, res.data.token);
|
|
128
|
+
localStorage.setItem(`nexa_user_${this.projectId}`, JSON.stringify(res.data.user));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return res.data;
|
|
132
|
+
},
|
|
133
|
+
signOut: () => {
|
|
134
|
+
this.token = null;
|
|
135
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
136
|
+
localStorage.removeItem(`nexa_token_${this.projectId}`);
|
|
137
|
+
localStorage.removeItem(`nexa_user_${this.projectId}`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
getCurrentUser: () => {
|
|
141
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
142
|
+
const userStr = localStorage.getItem(`nexa_user_${this.projectId}`);
|
|
143
|
+
return userStr ? JSON.parse(userStr) : null;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// -----------------------------------------------------------------
|
|
150
|
+
// Blob File Storage API
|
|
151
|
+
// -----------------------------------------------------------------
|
|
152
|
+
storage() {
|
|
153
|
+
return {
|
|
154
|
+
ref: (path = '') => ({
|
|
155
|
+
put: async (file) => {
|
|
156
|
+
const formData = new FormData();
|
|
157
|
+
formData.append('file', file);
|
|
158
|
+
const response = await this.client.post(`/api/project/${this.projectId}/storage/upload`, formData);
|
|
159
|
+
return response.data;
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// -----------------------------------------------------------------
|
|
165
|
+
// Offline Synchronization & Local IndexedDB Cache Engine
|
|
166
|
+
// -----------------------------------------------------------------
|
|
167
|
+
async _initFirestoreState() {
|
|
168
|
+
if (this._offlineQueueInitialized)
|
|
169
|
+
return;
|
|
170
|
+
this._offlineQueueInitialized = true;
|
|
171
|
+
this._isSyncing = false;
|
|
172
|
+
if (typeof window === 'undefined' || !window.indexedDB) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const dbName = `nexa_idb_${this.projectId}`;
|
|
176
|
+
this._idb = await new Promise((resolve, reject) => {
|
|
177
|
+
const req = indexedDB.open(dbName, 1);
|
|
178
|
+
req.onupgradeneeded = (e) => {
|
|
179
|
+
const db = e.target.result;
|
|
180
|
+
if (!db.objectStoreNames.contains('offline_queue')) {
|
|
181
|
+
db.createObjectStore('offline_queue', { keyPath: 'id' });
|
|
182
|
+
}
|
|
183
|
+
if (!db.objectStoreNames.contains('offline_cache')) {
|
|
184
|
+
db.createObjectStore('offline_cache', { keyPath: 'path' });
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
req.onsuccess = () => resolve(req.result);
|
|
188
|
+
req.onerror = () => reject(req.error);
|
|
189
|
+
});
|
|
190
|
+
await this._loadCache();
|
|
191
|
+
// Trigger queue sync periodically when browser returns online
|
|
192
|
+
if (typeof window !== 'undefined') {
|
|
193
|
+
window.addEventListener('online', () => this._syncOfflineQueue());
|
|
194
|
+
setTimeout(() => this._syncOfflineQueue(), 1000);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async _getOfflineQueue() {
|
|
198
|
+
if (!this._idb)
|
|
199
|
+
return [];
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
const tx = this._idb.transaction('offline_queue', 'readonly');
|
|
202
|
+
const store = tx.objectStore('offline_queue');
|
|
203
|
+
const req = store.getAll();
|
|
204
|
+
req.onsuccess = () => resolve(req.result);
|
|
205
|
+
req.onerror = () => reject(req.error);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async _saveOfflineQueue(queue) {
|
|
209
|
+
if (!this._idb)
|
|
210
|
+
return;
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const tx = this._idb.transaction('offline_queue', 'readwrite');
|
|
213
|
+
const store = tx.objectStore('offline_queue');
|
|
214
|
+
store.clear();
|
|
215
|
+
queue.forEach((item) => store.put(item));
|
|
216
|
+
tx.oncomplete = () => resolve();
|
|
217
|
+
tx.onerror = () => reject(tx.error);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async _addOfflineJob(job) {
|
|
221
|
+
if (!this._idb)
|
|
222
|
+
return;
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const tx = this._idb.transaction('offline_queue', 'readwrite');
|
|
225
|
+
const store = tx.objectStore('offline_queue');
|
|
226
|
+
store.put(job);
|
|
227
|
+
tx.oncomplete = () => {
|
|
228
|
+
resolve();
|
|
229
|
+
if (typeof navigator !== 'undefined' && navigator.onLine) {
|
|
230
|
+
this._syncOfflineQueue();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
tx.onerror = () => reject(tx.error);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async _setCache(path, data) {
|
|
237
|
+
this._firestoreCache[path] = data;
|
|
238
|
+
if (!this._idb)
|
|
239
|
+
return;
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const tx = this._idb.transaction('offline_cache', 'readwrite');
|
|
242
|
+
const store = tx.objectStore('offline_cache');
|
|
243
|
+
store.put({ path, data });
|
|
244
|
+
tx.oncomplete = () => resolve();
|
|
245
|
+
tx.onerror = () => reject(tx.error);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async _deleteCache(path) {
|
|
249
|
+
delete this._firestoreCache[path];
|
|
250
|
+
if (!this._idb)
|
|
251
|
+
return;
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const tx = this._idb.transaction('offline_cache', 'readwrite');
|
|
254
|
+
const store = tx.objectStore('offline_cache');
|
|
255
|
+
store.delete(path);
|
|
256
|
+
tx.oncomplete = () => resolve();
|
|
257
|
+
tx.onerror = () => reject(tx.error);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async _loadCache() {
|
|
261
|
+
if (!this._idb)
|
|
262
|
+
return;
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const tx = this._idb.transaction('offline_cache', 'readonly');
|
|
265
|
+
const store = tx.objectStore('offline_cache');
|
|
266
|
+
const req = store.getAll();
|
|
267
|
+
req.onsuccess = () => {
|
|
268
|
+
req.result.forEach((item) => {
|
|
269
|
+
this._firestoreCache[item.path] = item.data;
|
|
270
|
+
});
|
|
271
|
+
resolve();
|
|
272
|
+
};
|
|
273
|
+
req.onerror = () => reject(req.error);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async _syncOfflineQueue() {
|
|
277
|
+
if (this._isSyncing)
|
|
278
|
+
return;
|
|
279
|
+
this._isSyncing = true;
|
|
280
|
+
const queue = await this._getOfflineQueue();
|
|
281
|
+
if (queue.length === 0) {
|
|
282
|
+
this._isSyncing = false;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const job of [...queue]) {
|
|
286
|
+
try {
|
|
287
|
+
if (job.type === 'set') {
|
|
288
|
+
await this.client.post(`/api/firestore/${this.projectId}/document`, { docPath: job.path, data: job.data });
|
|
289
|
+
}
|
|
290
|
+
else if (job.type === 'update') {
|
|
291
|
+
await this.client.patch(`/api/firestore/${this.projectId}/document`, { docPath: job.path, data: job.data });
|
|
292
|
+
}
|
|
293
|
+
else if (job.type === 'delete') {
|
|
294
|
+
await this.client.delete(`/api/firestore/${this.projectId}/document?docPath=${encodeURIComponent(job.path)}`);
|
|
295
|
+
}
|
|
296
|
+
const currentQueue = await this._getOfflineQueue();
|
|
297
|
+
await this._saveOfflineQueue(currentQueue.filter((q) => q.id !== job.id));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
break; // Stop syncing on network exceptions
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this._isSyncing = false;
|
|
304
|
+
if (this._snapshotCallbacks) {
|
|
305
|
+
Object.values(this._snapshotCallbacks).forEach((cb) => cb());
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
_ensureFirestoreSSE() {
|
|
309
|
+
this.connectRealtime();
|
|
310
|
+
if (this._firestoreListening)
|
|
311
|
+
return;
|
|
312
|
+
this._firestoreListening = true;
|
|
313
|
+
this._sseCallbacks.firestore_changed.push(async (change) => {
|
|
314
|
+
if (change.type === 'bulk_update') {
|
|
315
|
+
Object.values(this._snapshotCallbacks).forEach((cb) => cb());
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (change.type === 'document_written') {
|
|
319
|
+
await this._setCache(change.docPath, change.data);
|
|
320
|
+
}
|
|
321
|
+
else if (change.type === 'document_deleted') {
|
|
322
|
+
await this._deleteCache(change.docPath);
|
|
323
|
+
}
|
|
324
|
+
Object.keys(this._snapshotCallbacks).forEach((pathKey) => {
|
|
325
|
+
const cb = this._snapshotCallbacks[pathKey];
|
|
326
|
+
if (pathKey === change.docPath || change.docPath.startsWith(pathKey + '/')) {
|
|
327
|
+
cb(change);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
exports.NexaApp = NexaApp;
|
|
334
|
+
// -----------------------------------------------------------------
|
|
335
|
+
// Modular Firestore-like APIs (Firebase Modular pattern match)
|
|
336
|
+
// -----------------------------------------------------------------
|
|
337
|
+
const initializeApp = (config) => {
|
|
338
|
+
return new NexaApp(config);
|
|
339
|
+
};
|
|
340
|
+
exports.initializeApp = initializeApp;
|
|
341
|
+
const getFirestore = (app) => {
|
|
342
|
+
app._initPromise = app._initFirestoreState();
|
|
343
|
+
return app;
|
|
344
|
+
};
|
|
345
|
+
exports.getFirestore = getFirestore;
|
|
346
|
+
const collection = (db, collectionPath) => {
|
|
347
|
+
return { type: 'collection', path: collectionPath, db };
|
|
348
|
+
};
|
|
349
|
+
exports.collection = collection;
|
|
350
|
+
const doc = (dbOrCollection, ...pathSegments) => {
|
|
351
|
+
if ('type' in dbOrCollection && dbOrCollection.type === 'collection') {
|
|
352
|
+
return { type: 'document', path: `${dbOrCollection.path}/${pathSegments.join('/')}`, db: dbOrCollection.db };
|
|
353
|
+
}
|
|
354
|
+
return { type: 'document', path: pathSegments.join('/'), db: dbOrCollection };
|
|
355
|
+
};
|
|
356
|
+
exports.doc = doc;
|
|
357
|
+
const query = (queryObject, ...queryConstraints) => {
|
|
358
|
+
const q = {
|
|
359
|
+
type: 'query',
|
|
360
|
+
path: queryObject.path,
|
|
361
|
+
db: queryObject.db,
|
|
362
|
+
constraints: queryObject.type === 'query' ? [...(queryObject.constraints || [])] : []
|
|
363
|
+
};
|
|
364
|
+
q.constraints = [...(q.constraints || []), ...queryConstraints];
|
|
365
|
+
return q;
|
|
366
|
+
};
|
|
367
|
+
exports.query = query;
|
|
368
|
+
const where = (fieldPath, opStr, value) => {
|
|
369
|
+
return { type: 'where', fieldPath, opStr, value };
|
|
370
|
+
};
|
|
371
|
+
exports.where = where;
|
|
372
|
+
const orderBy = (fieldPath, directionStr = 'asc') => {
|
|
373
|
+
return { type: 'orderBy', fieldPath, directionStr };
|
|
374
|
+
};
|
|
375
|
+
exports.orderBy = orderBy;
|
|
376
|
+
const limit = (limitValue) => {
|
|
377
|
+
return { type: 'limit', limitValue };
|
|
378
|
+
};
|
|
379
|
+
exports.limit = limit;
|
|
380
|
+
const getDocs = async (queryOrCollection) => {
|
|
381
|
+
const db = queryOrCollection.db;
|
|
382
|
+
if (db._initPromise)
|
|
383
|
+
await db._initPromise;
|
|
384
|
+
const collectionPath = queryOrCollection.path;
|
|
385
|
+
const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
|
|
386
|
+
try {
|
|
387
|
+
const res = await db.client.post(`/api/firestore/${db.projectId}/query`, {
|
|
388
|
+
collectionPath,
|
|
389
|
+
constraints
|
|
390
|
+
});
|
|
391
|
+
const docs = res.data.documents || [];
|
|
392
|
+
for (const d of docs) {
|
|
393
|
+
await db._setCache(`${collectionPath}/${d.id}`, d.fields);
|
|
394
|
+
}
|
|
395
|
+
const docsArray = docs.map((d) => ({
|
|
396
|
+
id: d.id,
|
|
397
|
+
data: () => d.fields
|
|
398
|
+
}));
|
|
399
|
+
return {
|
|
400
|
+
docs: docsArray,
|
|
401
|
+
forEach: (cb) => docsArray.forEach(cb),
|
|
402
|
+
empty: docsArray.length === 0,
|
|
403
|
+
size: docsArray.length
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
const results = [];
|
|
408
|
+
Object.keys(db._firestoreCache).forEach((p) => {
|
|
409
|
+
if (p.startsWith(collectionPath + '/')) {
|
|
410
|
+
const parts = p.split('/');
|
|
411
|
+
results.push({
|
|
412
|
+
id: parts[parts.length - 1],
|
|
413
|
+
data: () => db._firestoreCache[p]
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
// Simple offline filters
|
|
418
|
+
let filtered = results;
|
|
419
|
+
for (const c of constraints) {
|
|
420
|
+
if (c.type === 'where' && c.fieldPath && c.opStr) {
|
|
421
|
+
filtered = filtered.filter((docItem) => {
|
|
422
|
+
const val = docItem.data()[c.fieldPath];
|
|
423
|
+
if (c.opStr === '==')
|
|
424
|
+
return val === c.value;
|
|
425
|
+
if (c.opStr === '>')
|
|
426
|
+
return val > c.value;
|
|
427
|
+
if (c.opStr === '<')
|
|
428
|
+
return val < c.value;
|
|
429
|
+
if (c.opStr === '>=')
|
|
430
|
+
return val >= c.value;
|
|
431
|
+
if (c.opStr === '<=')
|
|
432
|
+
return val <= c.value;
|
|
433
|
+
if (c.opStr === '!=')
|
|
434
|
+
return val !== c.value;
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
docs: filtered,
|
|
441
|
+
forEach: (cb) => filtered.forEach(cb),
|
|
442
|
+
empty: filtered.length === 0,
|
|
443
|
+
size: filtered.length
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
exports.getDocs = getDocs;
|
|
448
|
+
const getDoc = async (docRef) => {
|
|
449
|
+
const db = docRef.db;
|
|
450
|
+
if (db._initPromise)
|
|
451
|
+
await db._initPromise;
|
|
452
|
+
try {
|
|
453
|
+
const res = await db.client.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`);
|
|
454
|
+
const data = res.data.data;
|
|
455
|
+
await db._setCache(docRef.path, data);
|
|
456
|
+
return { exists: () => !!data, data: () => data };
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
const data = db._firestoreCache[docRef.path];
|
|
460
|
+
return { exists: () => !!data, data: () => data || null };
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
exports.getDoc = getDoc;
|
|
464
|
+
const setDoc = async (docRef, data) => {
|
|
465
|
+
const db = docRef.db;
|
|
466
|
+
if (db._initPromise)
|
|
467
|
+
await db._initPromise;
|
|
468
|
+
await db._setCache(docRef.path, data);
|
|
469
|
+
// Trigger snapshot listeners
|
|
470
|
+
Object.keys(db._snapshotCallbacks).forEach((pathKey) => {
|
|
471
|
+
const cb = db._snapshotCallbacks[pathKey];
|
|
472
|
+
if (pathKey === docRef.path || docRef.path.startsWith(pathKey + '/')) {
|
|
473
|
+
cb({ type: 'document_written', docPath: docRef.path, data });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
const jobId = Math.random().toString(36).substring(2, 9);
|
|
477
|
+
const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
478
|
+
if (!isOnline) {
|
|
479
|
+
await db._addOfflineJob({ id: jobId, type: 'set', path: docRef.path, data });
|
|
480
|
+
return { success: true, message: 'Offline: Tersimpan di cache lokal IndexedDB' };
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
try {
|
|
484
|
+
return (await db.client.post(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data })).data;
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
await db._addOfflineJob({ id: jobId, type: 'set', path: docRef.path, data });
|
|
488
|
+
return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline IndexedDB' };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
exports.setDoc = setDoc;
|
|
493
|
+
const updateDoc = async (docRef, data) => {
|
|
494
|
+
const db = docRef.db;
|
|
495
|
+
if (db._initPromise)
|
|
496
|
+
await db._initPromise;
|
|
497
|
+
const jobId = Math.random().toString(36).substring(2, 9);
|
|
498
|
+
const existingData = db._firestoreCache[docRef.path] || {};
|
|
499
|
+
const updatedData = { ...existingData, ...data };
|
|
500
|
+
await db._setCache(docRef.path, updatedData);
|
|
501
|
+
const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
502
|
+
if (!isOnline) {
|
|
503
|
+
await db._addOfflineJob({ id: jobId, type: 'update', path: docRef.path, data });
|
|
504
|
+
return { success: true, message: 'Offline: Tersimpan di cache lokal IndexedDB' };
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
try {
|
|
508
|
+
return (await db.client.patch(`/api/firestore/${db.projectId}/document`, { docPath: docRef.path, data })).data;
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
await db._addOfflineJob({ id: jobId, type: 'update', path: docRef.path, data });
|
|
512
|
+
return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline IndexedDB' };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
exports.updateDoc = updateDoc;
|
|
517
|
+
const deleteDoc = async (docRef) => {
|
|
518
|
+
const db = docRef.db;
|
|
519
|
+
if (db._initPromise)
|
|
520
|
+
await db._initPromise;
|
|
521
|
+
await db._deleteCache(docRef.path);
|
|
522
|
+
// Trigger snapshot listeners
|
|
523
|
+
Object.keys(db._snapshotCallbacks).forEach((pathKey) => {
|
|
524
|
+
const cb = db._snapshotCallbacks[pathKey];
|
|
525
|
+
if (pathKey === docRef.path || docRef.path.startsWith(pathKey + '/')) {
|
|
526
|
+
cb({ type: 'document_deleted', docPath: docRef.path });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const jobId = Math.random().toString(36).substring(2, 9);
|
|
530
|
+
const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
531
|
+
if (!isOnline) {
|
|
532
|
+
await db._addOfflineJob({ id: jobId, type: 'delete', path: docRef.path });
|
|
533
|
+
return { success: true };
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
try {
|
|
537
|
+
return (await db.client.delete(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`)).data;
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
await db._addOfflineJob({ id: jobId, type: 'delete', path: docRef.path });
|
|
541
|
+
return { success: true };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
exports.deleteDoc = deleteDoc;
|
|
546
|
+
const onSnapshot = (ref, callback) => {
|
|
547
|
+
const db = ref.db;
|
|
548
|
+
const initSnapshot = async () => {
|
|
549
|
+
if (db._initPromise)
|
|
550
|
+
await db._initPromise;
|
|
551
|
+
db._ensureFirestoreSSE();
|
|
552
|
+
const pathKey = ref.path;
|
|
553
|
+
const triggerCallback = async () => {
|
|
554
|
+
if (ref.type === 'collection') {
|
|
555
|
+
const docs = await (0, exports.getDocs)(ref);
|
|
556
|
+
callback(docs);
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
const docData = await (0, exports.getDoc)(ref);
|
|
560
|
+
callback(docData);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
triggerCallback();
|
|
564
|
+
const listenerId = Math.random().toString(36).substring(2, 9);
|
|
565
|
+
db._snapshotCallbacks[`${ref.type}_${listenerId}_${pathKey}`] = triggerCallback;
|
|
566
|
+
return () => {
|
|
567
|
+
delete db._snapshotCallbacks[`${ref.type}_${listenerId}_${pathKey}`];
|
|
568
|
+
};
|
|
569
|
+
};
|
|
570
|
+
let unsubscribe = () => { };
|
|
571
|
+
initSnapshot().then((unsub) => {
|
|
572
|
+
unsubscribe = unsub;
|
|
573
|
+
});
|
|
574
|
+
return () => unsubscribe();
|
|
575
|
+
};
|
|
576
|
+
exports.onSnapshot = onSnapshot;
|
|
577
|
+
const writeBatch = (db) => {
|
|
578
|
+
const operations = [];
|
|
579
|
+
const batchObj = {
|
|
580
|
+
set: (docRef, data) => {
|
|
581
|
+
operations.push({ type: 'set', path: docRef.path, data });
|
|
582
|
+
return batchObj;
|
|
583
|
+
},
|
|
584
|
+
update: (docRef, data) => {
|
|
585
|
+
operations.push({ type: 'update', path: docRef.path, data });
|
|
586
|
+
return batchObj;
|
|
587
|
+
},
|
|
588
|
+
delete: (docRef) => {
|
|
589
|
+
operations.push({ type: 'delete', path: docRef.path });
|
|
590
|
+
return batchObj;
|
|
591
|
+
},
|
|
592
|
+
commit: async () => {
|
|
593
|
+
if (db._initPromise)
|
|
594
|
+
await db._initPromise;
|
|
595
|
+
if (operations.length === 0)
|
|
596
|
+
return;
|
|
597
|
+
const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
|
598
|
+
// Update local cache optimistically
|
|
599
|
+
for (const op of operations) {
|
|
600
|
+
if (op.type === 'set' || op.type === 'update') {
|
|
601
|
+
const newData = { ...db._firestoreCache[op.path], ...op.data };
|
|
602
|
+
await db._setCache(op.path, newData);
|
|
603
|
+
Object.keys(db._snapshotCallbacks).forEach((pathKey) => {
|
|
604
|
+
const cb = db._snapshotCallbacks[pathKey];
|
|
605
|
+
if (pathKey === op.path || op.path.startsWith(pathKey + '/')) {
|
|
606
|
+
cb({ type: 'document_written', docPath: op.path, data: db._firestoreCache[op.path] });
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
else if (op.type === 'delete') {
|
|
611
|
+
await db._deleteCache(op.path);
|
|
612
|
+
Object.keys(db._snapshotCallbacks).forEach((pathKey) => {
|
|
613
|
+
const cb = db._snapshotCallbacks[pathKey];
|
|
614
|
+
if (pathKey === op.path || op.path.startsWith(pathKey + '/')) {
|
|
615
|
+
cb({ type: 'document_deleted', docPath: op.path });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (!isOnline) {
|
|
621
|
+
for (const op of operations) {
|
|
622
|
+
await db._addOfflineJob({
|
|
623
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
624
|
+
type: op.type,
|
|
625
|
+
path: op.path,
|
|
626
|
+
data: op.data
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return { success: true, message: 'Offline: Batch operations queued in IndexedDB' };
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
try {
|
|
633
|
+
return (await db.client.post(`/api/firestore/${db.projectId}/batch`, { operations })).data;
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
for (const op of operations) {
|
|
637
|
+
await db._addOfflineJob({
|
|
638
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
639
|
+
type: op.type,
|
|
640
|
+
path: op.path,
|
|
641
|
+
data: op.data
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return { success: true, message: 'Koneksi terganggu: Tersimpan di antrean offline IndexedDB' };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
return batchObj;
|
|
650
|
+
};
|
|
651
|
+
exports.writeBatch = writeBatch;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nexabase-console",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SDK Client resmi untuk NexaBase: Platform Sinkronisasi NoSQL, Realtime, File Storage, & Autentikasi Offline-First.",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"nexabase",
|
|
16
|
+
"firestore",
|
|
17
|
+
"realtime",
|
|
18
|
+
"database",
|
|
19
|
+
"offline-first",
|
|
20
|
+
"indexeddb",
|
|
21
|
+
"sync",
|
|
22
|
+
"otp",
|
|
23
|
+
"auth",
|
|
24
|
+
"storage"
|
|
25
|
+
],
|
|
26
|
+
"author": "NexaBase Team",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"axios": "^1.7.9"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|