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 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.
@@ -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
+ }