nexabase-console 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -39,11 +39,15 @@ export interface Query<T = any> {
39
39
  constraints?: QueryConstraint[];
40
40
  }
41
41
  export interface DocumentSnapshot<T = any> {
42
+ id: string;
43
+ ref: DocumentReference<T>;
42
44
  exists: () => boolean;
43
45
  data: () => T | null;
44
46
  }
45
47
  export interface QueryDocumentSnapshot<T = any> {
46
48
  id: string;
49
+ ref: DocumentReference<T>;
50
+ exists: () => boolean;
47
51
  data: () => T;
48
52
  }
49
53
  export interface QuerySnapshot<T = any> {
@@ -51,7 +55,7 @@ export interface QuerySnapshot<T = any> {
51
55
  forEach: (callback: (doc: QueryDocumentSnapshot<T>) => void) => void;
52
56
  empty: boolean;
53
57
  size: number;
54
- docChanges?: () => any[];
58
+ docChanges: () => any[];
55
59
  }
56
60
  export interface DatabaseReference {
57
61
  get: () => Promise<any>;
@@ -59,10 +63,16 @@ export interface DatabaseReference {
59
63
  onDataChanged: (callback: (data: any) => void) => void;
60
64
  }
61
65
  export interface StorageReference {
62
- put: (file: File | Blob) => Promise<{
66
+ name: string;
67
+ fullPath: string;
68
+ put: (file: File | Blob, onProgress?: (percent: number) => void) => Promise<{
63
69
  url: string;
64
70
  success: boolean;
65
71
  }>;
72
+ delete: () => Promise<{
73
+ success: boolean;
74
+ message?: string;
75
+ }>;
66
76
  }
67
77
  export interface WriteBatch {
68
78
  set: (docRef: DocumentReference, data: any) => WriteBatch;
@@ -111,6 +121,7 @@ export declare class NexaApp {
111
121
  };
112
122
  storage(): {
113
123
  ref: (path?: string) => StorageReference;
124
+ refFromURL: (url: string) => StorageReference;
114
125
  };
115
126
  _initFirestoreState(): Promise<void>;
116
127
  _getOfflineQueue(): Promise<OfflineJob[]>;
@@ -130,11 +141,15 @@ export declare const query: <T = any>(queryObject: Query<T> | CollectionReferenc
130
141
  export declare const where: (fieldPath: string, opStr: "==" | ">" | "<" | ">=" | "<=" | "!=", value: any) => QueryConstraint;
131
142
  export declare const orderBy: (fieldPath: string, directionStr?: "asc" | "desc") => QueryConstraint;
132
143
  export declare const limit: (limitValue: number) => QueryConstraint;
144
+ export declare const getCachedDocs: <T = any>(queryOrCollection: Query<T> | CollectionReference<T>) => QuerySnapshot<T>;
145
+ export declare const getCachedDoc: <T = any>(docRef: DocumentReference<T>) => DocumentSnapshot<T>;
133
146
  export declare const getDocs: <T = any>(queryOrCollection: Query<T> | CollectionReference<T>) => Promise<QuerySnapshot<T>>;
134
147
  export declare const getDoc: <T = any>(docRef: DocumentReference<T>) => Promise<DocumentSnapshot<T>>;
135
148
  export declare const setDoc: (docRef: DocumentReference, data: any) => Promise<any>;
136
149
  export declare const updateDoc: (docRef: DocumentReference, data: any) => Promise<any>;
137
150
  export declare const deleteDoc: (docRef: DocumentReference) => Promise<any>;
138
151
  export declare const onSnapshot: (ref: CollectionReference | DocumentReference | Query, callback: (snapshot: any) => void) => (() => void);
152
+ export declare const enableIndexedDbPersistence: (db: NexaApp) => Promise<void>;
153
+ export declare const enableOfflinePersistence: (db: NexaApp) => Promise<void>;
139
154
  export declare const writeBatch: (db: NexaApp) => WriteBatch;
140
155
  export {};
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
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;
6
+ exports.writeBatch = exports.enableOfflinePersistence = exports.enableIndexedDbPersistence = exports.onSnapshot = exports.deleteDoc = exports.updateDoc = exports.setDoc = exports.getDoc = exports.getDocs = exports.getCachedDoc = exports.getCachedDocs = exports.limit = exports.orderBy = exports.where = exports.query = exports.doc = exports.collection = exports.getFirestore = exports.initializeApp = exports.NexaApp = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  // -----------------------------------------------------------------
9
9
  // Main NexaApp SDK Class
@@ -29,18 +29,29 @@ class NexaApp {
29
29
  this.token = savedToken;
30
30
  }
31
31
  }
32
- this.endpoint = config.endpoint || 'https://db.nexabase.id';
32
+ // 3. Smart Endpoint Resolution: Auto-detect domain/host if none is specified
33
+ let defaultEndpoint = 'https://db.nexabase.id';
34
+ if (typeof window !== 'undefined' && window.location) {
35
+ // Default to the current page origin in dev/proxy preview environments so it auto-points to /api/*
36
+ defaultEndpoint = window.location.origin;
37
+ }
38
+ this.endpoint = config.endpoint || defaultEndpoint;
33
39
  this.client = axios_1.default.create({
34
40
  baseURL: this.endpoint,
35
41
  headers: {
36
42
  'Content-Type': 'application/json'
37
43
  }
38
44
  });
39
- // Request interceptor to automatically attach authorization Bearer token
45
+ // Request interceptor to attach bearer token and automatically resolve the axios boundary issue for file uploads
40
46
  this.client.interceptors.request.use((req) => {
41
47
  if (this.token) {
42
48
  req.headers.Authorization = `Bearer ${this.token}`;
43
49
  }
50
+ // 2. Fix Axios Multipart Boundary on File Upload
51
+ // If Content-Type is multipart/form-data, delete it so axios/browser automatically appends the boundary header
52
+ if (req.headers && req.headers['Content-Type'] === 'multipart/form-data') {
53
+ delete req.headers['Content-Type'];
54
+ }
44
55
  return req;
45
56
  });
46
57
  }
@@ -151,14 +162,36 @@ class NexaApp {
151
162
  // -----------------------------------------------------------------
152
163
  storage() {
153
164
  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
- })
165
+ ref: (path = '') => {
166
+ const name = path.substring(path.lastIndexOf('/') + 1) || path;
167
+ return {
168
+ name,
169
+ fullPath: path,
170
+ put: async (file, onProgress) => {
171
+ const formData = new FormData();
172
+ formData.append('file', file);
173
+ const response = await this.client.post(`/api/project/${this.projectId}/storage/upload`, formData, {
174
+ // We omit Content-Type headers here (interceptor strips it anyway) so the browser naturally generates the boundary
175
+ onUploadProgress: (progressEvent) => {
176
+ if (onProgress && progressEvent.total) {
177
+ const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
178
+ onProgress(percent);
179
+ }
180
+ }
181
+ });
182
+ return response.data;
183
+ },
184
+ delete: async () => {
185
+ const response = await this.client.delete(`/api/project/${this.projectId}/storage/${encodeURIComponent(name)}`);
186
+ return response.data;
187
+ }
188
+ };
189
+ },
190
+ refFromURL: (url) => {
191
+ const cleanUrl = url.split('?')[0];
192
+ const filename = decodeURIComponent(cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1));
193
+ return this.storage().ref(filename);
194
+ }
162
195
  };
163
196
  }
164
197
  // -----------------------------------------------------------------
@@ -379,6 +412,70 @@ const limit = (limitValue) => {
379
412
  return { type: 'limit', limitValue };
380
413
  };
381
414
  exports.limit = limit;
415
+ // 1. Helper to retrieve docs from the local Offline cache (Cache-First)
416
+ const getCachedDocs = (queryOrCollection) => {
417
+ const db = queryOrCollection.db;
418
+ const collectionPath = queryOrCollection.path;
419
+ const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
420
+ const results = [];
421
+ Object.keys(db._firestoreCache).forEach((p) => {
422
+ if (p.startsWith(collectionPath + '/')) {
423
+ const parts = p.split('/');
424
+ const docId = parts[parts.length - 1];
425
+ const docRef = (0, exports.doc)(db, `${collectionPath}/${docId}`);
426
+ results.push({
427
+ id: docId,
428
+ ref: docRef,
429
+ exists: () => true,
430
+ data: () => db._firestoreCache[p]
431
+ });
432
+ }
433
+ });
434
+ // Simple offline filters
435
+ let filtered = results;
436
+ for (const c of constraints) {
437
+ if (c.type === 'where' && c.fieldPath && c.opStr) {
438
+ filtered = filtered.filter((docItem) => {
439
+ const val = docItem.data()[c.fieldPath];
440
+ if (c.opStr === '==')
441
+ return val === c.value;
442
+ if (c.opStr === '>')
443
+ return val > c.value;
444
+ if (c.opStr === '<')
445
+ return val < c.value;
446
+ if (c.opStr === '>=')
447
+ return val >= c.value;
448
+ if (c.opStr === '<=')
449
+ return val <= c.value;
450
+ if (c.opStr === '!=')
451
+ return val !== c.value;
452
+ return true;
453
+ });
454
+ }
455
+ }
456
+ return {
457
+ docs: filtered,
458
+ forEach: (cb) => filtered.forEach(cb),
459
+ empty: filtered.length === 0,
460
+ size: filtered.length,
461
+ docChanges: () => filtered.map(doc => ({ type: 'added', doc }))
462
+ };
463
+ };
464
+ exports.getCachedDocs = getCachedDocs;
465
+ // 1. Helper to retrieve single doc from the local Offline cache (Cache-First)
466
+ const getCachedDoc = (docRef) => {
467
+ const db = docRef.db;
468
+ const segments = docRef.path.split('/');
469
+ const docId = segments[segments.length - 1];
470
+ const data = db._firestoreCache[docRef.path];
471
+ return {
472
+ id: docId,
473
+ ref: docRef,
474
+ exists: () => !!data,
475
+ data: () => (data || null)
476
+ };
477
+ };
478
+ exports.getCachedDoc = getCachedDoc;
382
479
  const getDocs = async (queryOrCollection) => {
383
480
  const db = queryOrCollection.db;
384
481
  if (db._initPromise)
@@ -394,10 +491,15 @@ const getDocs = async (queryOrCollection) => {
394
491
  for (const d of docs) {
395
492
  await db._setCache(`${collectionPath}/${d.id}`, d.fields);
396
493
  }
397
- const docsArray = docs.map((d) => ({
398
- id: d.id,
399
- data: () => d.fields
400
- }));
494
+ const docsArray = docs.map((d) => {
495
+ const docRef = (0, exports.doc)(db, `${collectionPath}/${d.id}`);
496
+ return {
497
+ id: d.id,
498
+ ref: docRef,
499
+ exists: () => true,
500
+ data: () => d.fields
501
+ };
502
+ });
401
503
  return {
402
504
  docs: docsArray,
403
505
  forEach: (cb) => docsArray.forEach(cb),
@@ -410,54 +512,7 @@ const getDocs = async (queryOrCollection) => {
410
512
  if (err && err.response) {
411
513
  throw err;
412
514
  }
413
- const results = [];
414
- Object.keys(db._firestoreCache).forEach((p) => {
415
- if (p.startsWith(collectionPath + '/')) {
416
- const parts = p.split('/');
417
- results.push({
418
- id: parts[parts.length - 1],
419
- data: () => db._firestoreCache[p]
420
- });
421
- }
422
- });
423
- // Simple offline filters
424
- let filtered = results;
425
- for (const c of constraints) {
426
- if (c.type === 'where' && c.fieldPath && c.opStr) {
427
- filtered = filtered.filter((docItem) => {
428
- const val = docItem.data()[c.fieldPath];
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
- if (c.opStr === '>=')
436
- return val >= c.value;
437
- if (c.opStr === '<=')
438
- return val <= c.value;
439
- if (c.opStr === '!=')
440
- return val !== c.value;
441
- if (c.opStr === 'in')
442
- return Array.isArray(c.value) && c.value.includes(val);
443
- if (c.opStr === 'not-in')
444
- return Array.isArray(c.value) && !c.value.includes(val);
445
- if (c.opStr === 'array-contains')
446
- return Array.isArray(val) && val.includes(c.value);
447
- if (c.opStr === 'array-contains-any') {
448
- return Array.isArray(val) && Array.isArray(c.value) && val.some((v) => c.value.includes(v));
449
- }
450
- return true;
451
- });
452
- }
453
- }
454
- return {
455
- docs: filtered,
456
- forEach: (cb) => filtered.forEach(cb),
457
- empty: filtered.length === 0,
458
- size: filtered.length,
459
- docChanges: () => filtered.map(doc => ({ type: 'added', doc }))
460
- };
515
+ return (0, exports.getCachedDocs)(queryOrCollection);
461
516
  }
462
517
  };
463
518
  exports.getDocs = getDocs;
@@ -465,18 +520,24 @@ const getDoc = async (docRef) => {
465
520
  const db = docRef.db;
466
521
  if (db._initPromise)
467
522
  await db._initPromise;
523
+ const segments = docRef.path.split('/');
524
+ const docId = segments[segments.length - 1];
468
525
  try {
469
526
  const res = await db.client.get(`/api/firestore/${db.projectId}/document?docPath=${encodeURIComponent(docRef.path)}`);
470
527
  const data = res.data.data;
471
528
  await db._setCache(docRef.path, data);
472
- return { exists: () => !!data, data: () => data };
529
+ return {
530
+ id: docId,
531
+ ref: docRef,
532
+ exists: () => !!data,
533
+ data: () => data
534
+ };
473
535
  }
474
536
  catch (err) {
475
537
  if (err && err.response) {
476
538
  throw err;
477
539
  }
478
- const data = db._firestoreCache[docRef.path];
479
- return { exists: () => !!data, data: () => (data || null) };
540
+ return (0, exports.getCachedDoc)(docRef);
480
541
  }
481
542
  };
482
543
  exports.getDoc = getDoc;
@@ -573,6 +634,19 @@ const onSnapshot = (ref, callback) => {
573
634
  await db._initPromise;
574
635
  db._ensureFirestoreSSE();
575
636
  const pathKey = ref.path;
637
+ // 1. Native Offline Cache/Persistence Priority: Emits cached snapshot IMMEDIATELY to trigger instant optimistic rendering!
638
+ if (ref.type === 'collection' || ref.type === 'query') {
639
+ const cached = (0, exports.getCachedDocs)(ref);
640
+ if (cached && cached.docs.length > 0) {
641
+ callback(cached);
642
+ }
643
+ }
644
+ else {
645
+ const cached = (0, exports.getCachedDoc)(ref);
646
+ if (cached && cached.exists()) {
647
+ callback(cached);
648
+ }
649
+ }
576
650
  const triggerCallback = async () => {
577
651
  if (ref.type === 'collection' || ref.type === 'query') {
578
652
  const docs = await (0, exports.getDocs)(ref);
@@ -583,6 +657,7 @@ const onSnapshot = (ref, callback) => {
583
657
  callback(docData);
584
658
  }
585
659
  };
660
+ // 2. Fetch the latest live server data and update cache + trigger the callback again
586
661
  triggerCallback();
587
662
  const listenerId = Math.random().toString(36).substring(2, 9);
588
663
  db._snapshotCallbacks[`${ref.type}_${listenerId}_${pathKey}`] = triggerCallback;
@@ -597,6 +672,17 @@ const onSnapshot = (ref, callback) => {
597
672
  return () => unsubscribe();
598
673
  };
599
674
  exports.onSnapshot = onSnapshot;
675
+ // Supporting standard Firebase modular options functions
676
+ const enableIndexedDbPersistence = async (db) => {
677
+ if (db._initPromise)
678
+ await db._initPromise;
679
+ };
680
+ exports.enableIndexedDbPersistence = enableIndexedDbPersistence;
681
+ const enableOfflinePersistence = async (db) => {
682
+ if (db._initPromise)
683
+ await db._initPromise;
684
+ };
685
+ exports.enableOfflinePersistence = enableOfflinePersistence;
600
686
  const writeBatch = (db) => {
601
687
  const operations = [];
602
688
  const batchObj = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexabase-console",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "SDK Client resmi untuk NexaBase: Platform Sinkronisasi NoSQL, Realtime, File Storage, & Autentikasi Offline-First.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",