postbase 0.1.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.
Files changed (126) hide show
  1. package/.github/workflows/test.yml +74 -0
  2. package/CLA.md +60 -0
  3. package/CONTRIBUTORS.md +35 -0
  4. package/LICENSE +661 -0
  5. package/README.md +211 -0
  6. package/admin/404.html +33 -0
  7. package/admin/README.md +21 -0
  8. package/admin/index.html +15 -0
  9. package/admin/jsconfig.json +20 -0
  10. package/admin/lib/postbase.js +222 -0
  11. package/admin/package-lock.json +3746 -0
  12. package/admin/package.json +27 -0
  13. package/admin/public/assets/img/admin-ui.png +0 -0
  14. package/admin/public/assets/img/blank-profile-picture-960_720.webp +0 -0
  15. package/admin/public/assets/img/chart-active-users.png +0 -0
  16. package/admin/public/assets/img/icon-transparent.png +0 -0
  17. package/admin/src/App.jsx +48 -0
  18. package/admin/src/auth.js +11 -0
  19. package/admin/src/common/formatDateTime.js +18 -0
  20. package/admin/src/components/AuthPanel.jsx +88 -0
  21. package/admin/src/components/Header.jsx +67 -0
  22. package/admin/src/main.jsx +6 -0
  23. package/admin/src/pages/Dashboard.jsx +24 -0
  24. package/admin/src/pages/Home.jsx +52 -0
  25. package/admin/src/pages/Login.jsx +10 -0
  26. package/admin/src/pages/authentication/Users.jsx +199 -0
  27. package/admin/src/pages/firestore/Database.jsx +29 -0
  28. package/admin/src/pages/storage/files.jsx +29 -0
  29. package/admin/src/postbase.js +15 -0
  30. package/admin/src/styles.css +3 -0
  31. package/admin/tailwind.config.cjs +11 -0
  32. package/admin/template.env +2 -0
  33. package/admin/vite.config.js +21 -0
  34. package/assets/img/HomePageScreenshot.png +0 -0
  35. package/assets/img/better-auth-logo-dark.136b122f.png +0 -0
  36. package/assets/img/better-auth-logo-light.4b03f444.png +0 -0
  37. package/assets/img/expresjs.png +0 -0
  38. package/assets/img/icon-transparent.png +0 -0
  39. package/assets/img/icon.png +0 -0
  40. package/assets/img/letsencrypt-logo-horizontal.png +0 -0
  41. package/assets/img/logo.png +0 -0
  42. package/assets/img/node.js_logo.png +0 -0
  43. package/assets/img/nodejsLight.svg +39 -0
  44. package/assets/img/postgres.png +0 -0
  45. package/backend/README.md +49 -0
  46. package/backend/admin/auth.js +9 -0
  47. package/backend/app.js +68 -0
  48. package/backend/auth.js +92 -0
  49. package/backend/env.js +12 -0
  50. package/backend/lib/postbase/adminClient.js +520 -0
  51. package/backend/lib/postbase/compat/admin.js +44 -0
  52. package/backend/lib/postbase/db.js +17 -0
  53. package/backend/lib/postbase/genericRouter.js +603 -0
  54. package/backend/lib/postbase/local-storage.js +56 -0
  55. package/backend/lib/postbase/metadataCache.js +32 -0
  56. package/backend/lib/postbase/middlewares/auth.js +57 -0
  57. package/backend/lib/postbase/migrations/1765239687559_rtdb-nodes.js +93 -0
  58. package/backend/lib/postbase/package-lock.json +5873 -0
  59. package/backend/lib/postbase/package.json +19 -0
  60. package/backend/lib/postbase/rtdb/router.js +190 -0
  61. package/backend/lib/postbase/rtdb/rulesEngine.js +63 -0
  62. package/backend/lib/postbase/rtdb/ws.js +84 -0
  63. package/backend/lib/postbase/rulesEngine.js +62 -0
  64. package/backend/lib/postbase/storage.js +130 -0
  65. package/backend/lib/postbase/tests/README.md +22 -0
  66. package/backend/lib/postbase/tests/db.js +9 -0
  67. package/backend/lib/postbase/tests/rtdb.rest.test.js +46 -0
  68. package/backend/lib/postbase/tests/rtdb.ws.test.js +113 -0
  69. package/backend/lib/postbase/tests/rules.js +26 -0
  70. package/backend/lib/postbase/tests/testServer.js +46 -0
  71. package/backend/lib/postbase/websocket.js +131 -0
  72. package/backend/local.js +6 -0
  73. package/backend/main.js +20 -0
  74. package/backend/middlewares/auth_middleware.js +10 -0
  75. package/backend/migrations/1762137399366-init.sql +98 -0
  76. package/backend/migrations/1762137399367_init_jsonb_schema.js +68 -0
  77. package/backend/migrations/1762149999999_enable_realtime_changes.js +48 -0
  78. package/backend/migrations/1765224247654_rtdb-nodes.js +93 -0
  79. package/backend/package-lock.json +2374 -0
  80. package/backend/package.json +27 -0
  81. package/backend/postbase_db_rules.js +128 -0
  82. package/backend/postbase_rtdb_rules.js +27 -0
  83. package/backend/postbase_storage_rules.js +45 -0
  84. package/backend/template.env +10 -0
  85. package/backend-systemd/README.md +39 -0
  86. package/backend-systemd/your_website.com.service +12 -0
  87. package/frontend/404.html +33 -0
  88. package/frontend/README.md +25 -0
  89. package/frontend/index.html +15 -0
  90. package/frontend/jsconfig.json +20 -0
  91. package/frontend/lib/postbase/auth.js +132 -0
  92. package/frontend/lib/postbase/compat/firebase/app.js +3 -0
  93. package/frontend/lib/postbase/compat/firebase/auth.js +115 -0
  94. package/frontend/lib/postbase/compat/firebase/database.js +11 -0
  95. package/frontend/lib/postbase/compat/firebase/firestore/lite.js +61 -0
  96. package/frontend/lib/postbase/compat/firebase/storage.js +10 -0
  97. package/frontend/lib/postbase/db.js +657 -0
  98. package/frontend/lib/postbase/package-lock.json +6284 -0
  99. package/frontend/lib/postbase/package.json +17 -0
  100. package/frontend/lib/postbase/rtdb.js +108 -0
  101. package/frontend/lib/postbase/storage.js +293 -0
  102. package/frontend/lib/postbase/tests/rtdb.client.test.js +88 -0
  103. package/frontend/lib/postbase/tests/waitFor.js +13 -0
  104. package/frontend/lib/postbase/utils.js +1 -0
  105. package/frontend/package-lock.json +2977 -0
  106. package/frontend/package.json +24 -0
  107. package/frontend/src/App.jsx +38 -0
  108. package/frontend/src/auth.js +52 -0
  109. package/frontend/src/components/AuthPanel.jsx +85 -0
  110. package/frontend/src/components/Header.jsx +54 -0
  111. package/frontend/src/main.jsx +5 -0
  112. package/frontend/src/pages/Dashboard.jsx +24 -0
  113. package/frontend/src/pages/Home.jsx +178 -0
  114. package/frontend/src/pages/Login.jsx +10 -0
  115. package/frontend/src/postbase.js +14 -0
  116. package/frontend/src/styles.css +1 -0
  117. package/frontend/tailwind.config.cjs +11 -0
  118. package/frontend/template.env +2 -0
  119. package/frontend/vite.config.js +18 -0
  120. package/git/hooks/README.md +31 -0
  121. package/git/hooks/post-receive +26 -0
  122. package/nginx/README.md +84 -0
  123. package/nginx/apt/www.your_website.com.conf +80 -0
  124. package/nginx/homebrew/www.your_website.com.conf +80 -0
  125. package/nginx/letsencrypt/README +14 -0
  126. package/package.json +8 -0
@@ -0,0 +1,657 @@
1
+ // // Minimal client SDK for the generic CRUD API.
2
+ // // Usage example:
3
+ // import { getDB } from './postbase.js';
4
+ // const db = getDB({ baseUrl: 'https://api.example.com/api' });
5
+ // const posts = db.collection('posts');
6
+ // await posts.add({ title: 'hi' });
7
+ // const doc = await posts.doc('123').get();
8
+ //
9
+ // // You can also use references
10
+ // const userRef = db.collection('users').doc('alovelace');
11
+ // await userRef.set({ name: 'Ada Lovelace' });
12
+
13
+ // await db.collection('reviews').doc('r1').set({
14
+ // rating: 5,
15
+ // reviewer: userRef
16
+ // });
17
+
18
+ // const review = await db.collection('reviews').doc('r1').get();
19
+
20
+ // console.log(review.reviewer instanceof DocumentReference); // ✅ true
21
+ // const user = await review.reviewer.get();
22
+ // console.log(user.name); // "Ada Lovelace"
23
+
24
+ function toJsonOrThrow(res) {
25
+ if (!res.ok) {
26
+ return res.json().then(j => { throw j; });
27
+ }
28
+ return res.json();
29
+ }
30
+
31
+ export function getDB({
32
+ baseUrl = '/api/db',
33
+ defaultHeaders = {},
34
+ getAuthToken = null, // 👈 optional async token resolver
35
+ } = {}) {
36
+ return new Database(baseUrl.replace(/\/$/, ''), defaultHeaders, getAuthToken);
37
+ }
38
+
39
+ class Database {
40
+ constructor(baseUrl, defaultHeaders, getAuthToken) {
41
+ this.baseUrl = baseUrl;
42
+ this.defaultHeaders = defaultHeaders;
43
+ this.getAuthToken = getAuthToken;
44
+ }
45
+
46
+ collection(name) {
47
+ return new CollectionReference(this, name);
48
+ }
49
+
50
+ async getHeaders() {
51
+ const headers = { ...this.defaultHeaders };
52
+ if (typeof this.getAuthToken === 'function') {
53
+ try {
54
+ const token = await this.getAuthToken();
55
+ if (token) headers['Authorization'] = `Bearer ${token}`;
56
+ } catch (err) {
57
+ console.warn('getAuthToken failed', err);
58
+ }
59
+ }
60
+ return headers;
61
+ }
62
+ }
63
+
64
+ class CollectionReference {
65
+ constructor(db, name, parentPath = null) {
66
+ this.db = db;
67
+ this.name = name;
68
+ this.parentPath = parentPath; // e.g., "users/u1"
69
+ }
70
+
71
+ get fullPath() {
72
+ return this.parentPath ? `${this.parentPath}/${encodeURIComponent(this.name)}` : encodeURIComponent(this.name);
73
+ }
74
+
75
+ doc(id) {
76
+ return new DocumentReference(this.db, this.name, id, this.parentPath);
77
+ }
78
+
79
+ collection(subName) {
80
+ return new CollectionReference(this.db, subName, this.fullPath);
81
+ }
82
+
83
+ async add(data) {
84
+ const url = `${this.db.baseUrl}/${this.fullPath}`;
85
+ const headers = await this.db.getHeaders();
86
+ const res = await fetch(url, {
87
+ method: 'POST',
88
+ headers: { 'content-type': 'application/json', ...headers },
89
+ body: JSON.stringify(serializeRefs(data))
90
+ });
91
+ const json = await toJsonOrThrow(res);
92
+ const _data = json.data;
93
+ if (_data && _data.hasOwnProperty('id')) {
94
+ return this.doc(_data.id);
95
+ }
96
+ return _data;
97
+ }
98
+
99
+ /** Directly get all docs (no filters) */
100
+ async get() {
101
+ const query = new QueryBuilder(this);
102
+ const data = await query.get();
103
+ const { docs } = data;
104
+ // If there are no query filters, wrap in QuerySnapshot
105
+ if (query._filters.length === 0 && query._order.length === 0 && !query._limit) {
106
+ return new QuerySnapshot(docs);
107
+ }
108
+ // Otherwise, return array of DocumentSnapshot as before
109
+ return data;
110
+ }
111
+
112
+ /** Start a query builder chain */
113
+ where(field, op, value) {
114
+ return new QueryBuilder(this).where(field, op, value);
115
+ }
116
+
117
+ /** Optional syntactic sugar — allow orderBy/limit without where() */
118
+ orderBy(field, dir = 'asc') {
119
+ return new QueryBuilder(this).orderBy(field, dir);
120
+ }
121
+
122
+ limit(n) {
123
+ return new QueryBuilder(this).limit(n);
124
+ }
125
+ }
126
+
127
+
128
+ /**
129
+ * Recursively convert DocumentReference instances to JSON-safe { _type: 'ref', path } objects
130
+ */
131
+ function serializeRefs(obj) {
132
+ if (obj instanceof DocumentReference) {
133
+ return { _type: 'ref', path: obj.fullPath };
134
+ }
135
+
136
+ //NEW: If Timestamp instance, send canonical structure
137
+ if (obj instanceof Timestamp) {
138
+ return obj.toString();
139
+ }
140
+
141
+ if (Array.isArray(obj)) return obj.map(serializeRefs);
142
+
143
+ if (obj && typeof obj === 'object') {
144
+ const out = {};
145
+ for (const [k, v] of Object.entries(obj)) {
146
+ out[k] = serializeRefs(v);
147
+ }
148
+ return out;
149
+ }
150
+
151
+ return obj;
152
+ }
153
+
154
+ /**
155
+ * Recursively restore { _type:'ref', path } objects back to DocumentReference instances.
156
+ */
157
+ function deserializeRefs(db, obj) {
158
+ if (Array.isArray(obj)) {
159
+ return obj.map(v => deserializeRefs(db, v));
160
+ }
161
+
162
+ if (obj && typeof obj === 'object') {
163
+ // Detect PostgreSQL TIMESTAMPTZ returned as strings
164
+ if (isIsoDateString(obj)) {
165
+ return Timestamp.fromPostgres(obj);
166
+ }
167
+
168
+ if (obj._type === 'ref' && obj.path) {
169
+ const parts = obj.path.split('/');
170
+ let ref = db.collection(parts[0]).doc(parts[1]);
171
+ for (let i = 2; i < parts.length; i += 2) {
172
+ ref = ref.collection(parts[i]).doc(parts[i + 1]);
173
+ }
174
+ return ref;
175
+ }
176
+
177
+ const out = {};
178
+ for (const [k, v] of Object.entries(obj)) {
179
+ out[k] = deserializeRefs(db, v);
180
+ }
181
+ return out;
182
+ }
183
+
184
+ //If a primitive ISO string is received, convert it
185
+ if (isIsoDateString(obj)) {
186
+ return Timestamp.fromPostgres(obj);
187
+ }
188
+
189
+ return obj;
190
+ }
191
+
192
+ class DocumentsSnapshot {
193
+ constructor(docs) {
194
+ this.docs = docs; // array of DocumentSnapshot
195
+ }
196
+
197
+ forEach(callback) {
198
+ this.docs.forEach(callback);
199
+ }
200
+
201
+ map(callback) {
202
+ return this.docs.map(callback);
203
+ }
204
+
205
+ get empty() {
206
+ return this.docs.length === 0;
207
+ }
208
+
209
+ get size() {
210
+ return this.docs.length;
211
+ }
212
+ }
213
+
214
+ class DocumentSnapshot {
215
+ constructor(id, data, path, db) {
216
+ this.id = decodeURIComponent(id);
217
+ this._path = path.split('/').map(decodeURIComponent).join('/');
218
+ this._db = db;
219
+ this._data = data;
220
+ }
221
+
222
+ data() {
223
+ return this._data;
224
+ }
225
+
226
+ get ref() {
227
+ const parts = this._path.split('/');
228
+ let ref = this._db.collection(parts[0]).doc(parts[1]);
229
+ for (let i = 2; i < parts.length; i += 2) {
230
+ ref = ref.collection(parts[i]).doc(parts[i + 1]);
231
+ }
232
+ return ref;
233
+ }
234
+
235
+ get path() {
236
+ return this._path;
237
+ }
238
+
239
+ get exists() {
240
+ return !!this._data;
241
+ }
242
+ }
243
+
244
+ class DocumentReference {
245
+ constructor(db, collectionName, id, parentPath = null) {
246
+ this.db = db;
247
+ this.collectionName = collectionName;
248
+ this.id = id;
249
+ this.parentPath = parentPath;
250
+ }
251
+
252
+ /** Full document path, e.g. "users/u1/posts/p2" */
253
+ get fullPath() {
254
+ const base = this.parentPath ? `${this.parentPath}/${encodeURIComponent(this.collectionName)}` : encodeURIComponent(this.collectionName);
255
+ return `${base}/${encodeURIComponent(this.id)}`;
256
+ }
257
+
258
+ /** Allow chaining subcollections under this document */
259
+ collection(subName) {
260
+ return new CollectionReference(this.db, subName, this.fullPath);
261
+ }
262
+
263
+ async get() {
264
+ const url = `${this.db.baseUrl}/${this.fullPath}`;
265
+ const headers = await this.db.getHeaders();
266
+ const res = await fetch(url, { headers });
267
+ if (!res.ok) {
268
+ const errorData = await res.json();
269
+ console.error(`HTTP error! Status: ${res.status}, Details: ${JSON.stringify(errorData)}`);
270
+ if (errorData.hasOwnProperty('error')) {
271
+ return new DocumentSnapshot(this.id, null, this.fullPath, this.db);
272
+ }
273
+ }
274
+ const json = await toJsonOrThrow(res);
275
+ const data = deserializeRefs(this.db, json.data || {});
276
+ return new DocumentSnapshot(this.id, data, this.fullPath, this.db);
277
+ }
278
+
279
+ async set(data, opts = {}) {
280
+ const url = `${this.db.baseUrl}/${this.fullPath}`;
281
+ const headers = await this.db.getHeaders();
282
+
283
+ // If merge=true, fetch existing data first and merge locally
284
+ let finalData = data;
285
+ if (opts.merge) {
286
+ try {
287
+ const existing = await this.get();
288
+ finalData = { ...(existing.data() || {}), ...data };
289
+ } catch (err) {
290
+ // If doc doesn't exist, just create it
291
+ finalData = data;
292
+ }
293
+ }
294
+
295
+ const res = await fetch(url, {
296
+ method: 'PUT',
297
+ headers: { 'content-type': 'application/json', ...headers },
298
+ body: JSON.stringify(serializeRefs(finalData)),
299
+ });
300
+ const json = await toJsonOrThrow(res);
301
+ return json.data;
302
+ }
303
+
304
+ async update(data) {
305
+ const url = `${this.db.baseUrl}/${this.fullPath}`;
306
+ const headers = await this.db.getHeaders();
307
+ const res = await fetch(url, {
308
+ method: 'PATCH',
309
+ headers: { 'content-type': 'application/json', ...headers },
310
+ body: JSON.stringify(serializeRefs(data)),
311
+ });
312
+ const json = await toJsonOrThrow(res);
313
+ return json.data;
314
+ }
315
+
316
+ async delete() {
317
+ const url = `${this.db.baseUrl}/${this.fullPath}`;
318
+ const headers = await this.db.getHeaders();
319
+ const res = await fetch(url, { method: 'DELETE', headers });
320
+ const json = await toJsonOrThrow(res);
321
+ return json.data;
322
+ }
323
+
324
+ onSnapshot(callback, errorCallback) {
325
+ // Reuse the existing real-time query system
326
+ const q = new QueryBuilder(this.collection(this.id));
327
+ q._filters.push({ field: "__id", op: "==", value: this.id });
328
+
329
+ return q.onSnapshot((querySnap) => {
330
+ const docSnap = querySnap.docs[0] ||
331
+ new DocumentSnapshot(this.id, null, this.fullPath, this.db);
332
+ callback(docSnap);
333
+ }, errorCallback);
334
+ }
335
+ }
336
+
337
+ /* Query builder helpers */
338
+ export function query(collectionRef, ...clauses) {
339
+ // returns a structured query object for backend: { filters: [...], order: [...], limit, offset }
340
+ const q = new QueryBuilder(collectionRef);
341
+ for (const c of clauses) {
342
+ if (c instanceof QueryBuilder) q.mergeFrom(c);
343
+ }
344
+ return q;
345
+ }
346
+
347
+ class QuerySnapshot {
348
+ constructor(docs) {
349
+ this.docs = docs; // array of DocumentSnapshot
350
+ }
351
+
352
+ // Optional helper like Firestore
353
+ forEach(callback) {
354
+ this.docs.forEach(callback);
355
+ }
356
+
357
+ map(callback) {
358
+ return this.docs.map(callback);
359
+ }
360
+
361
+ get empty() {
362
+ return this.docs.length === 0;
363
+ }
364
+
365
+ get size() {
366
+ return this.docs.length;
367
+ }
368
+ }
369
+
370
+ class QueryBuilder {
371
+ constructor(collectionRef) {
372
+ this.collectionRef = collectionRef;
373
+ this._filters = [];
374
+ this._order = [];
375
+ this._limit = undefined;
376
+ this._offset = undefined;
377
+
378
+ // New cursor fields
379
+ this._startAt = undefined;
380
+ this._startAfter = undefined;
381
+ this._endAt = undefined;
382
+ this._endBefore = undefined;
383
+ }
384
+
385
+ where(field, op, value) {
386
+ let _value = value;
387
+ if (typeof value === 'object'
388
+ && value !== null
389
+ && value.hasOwnProperty('_type')
390
+ && value._type === 'timestamp'
391
+ && 'toString' in value) {
392
+ _value = value.toString();
393
+ } else if (value instanceof DocumentReference) {
394
+ _value = { _type: 'ref', path: value.fullPath };
395
+ }
396
+ this._filters.push({ field, op, value: _value });
397
+ return this;
398
+ }
399
+
400
+ orderBy(field, dir = 'asc') {
401
+ this._order.push({ field, dir });
402
+ return this;
403
+ }
404
+
405
+ limit(n) {
406
+ this._limit = n;
407
+ return this;
408
+ }
409
+
410
+ offset(n) {
411
+ this._offset = n;
412
+ return this;
413
+ }
414
+
415
+ // 🔽 Pagination methods (Firestore-like)
416
+ startAt(cursor) {
417
+ this._startAt = this._normalizeCursor(cursor);
418
+ return this;
419
+ }
420
+
421
+ startAfter(cursor) {
422
+ this._startAfter = this._normalizeCursor(cursor);
423
+ return this;
424
+ }
425
+
426
+ endAt(cursor) {
427
+ this._endAt = this._normalizeCursor(cursor);
428
+ return this;
429
+ }
430
+
431
+ endBefore(cursor) {
432
+ this._endBefore = this._normalizeCursor(cursor);
433
+ return this;
434
+ }
435
+
436
+ // Internal helper — Firestore allows passing either value or doc snapshot
437
+ _normalizeCursor(cursor) {
438
+ if (!cursor) return null;
439
+ if (typeof cursor === 'object' && cursor.id) {
440
+ // Likely a document snapshot or data with an id
441
+ return { _type: 'cursorRef', path: cursor._path || `${this.collectionRef.fullPath}/${cursor.id}` };
442
+ }
443
+ // For raw scalar values (like numeric sort fields)
444
+ return cursor;
445
+ }
446
+
447
+ build() {
448
+ const out = {};
449
+ if (this._filters.length) out.filters = this._filters;
450
+ if (this._order.length) out.order = this._order;
451
+ if (typeof this._limit !== 'undefined') out.limit = this._limit;
452
+ if (typeof this._offset !== 'undefined') out.offset = this._offset;
453
+
454
+ if (this.collectionRef.parentPath) {
455
+ // parentPath = "messages/A"
456
+ const parts = this.collectionRef.parentPath.split('/');
457
+ const parentTable = parts[0];
458
+ const parentId = parts[1];
459
+ out.parent = {
460
+ collectionName: parentTable,
461
+ id: parentId,
462
+ path: `${this.collectionRef.parentPath}`
463
+ };
464
+ }
465
+
466
+ return out;
467
+ }
468
+
469
+ async get() {
470
+ const url = `${this.collectionRef.db.baseUrl}/${this.collectionRef.fullPath}/query`;
471
+ const headers = await this.collectionRef.db.getHeaders();
472
+
473
+ const queryBody = this.build();
474
+ const res = await fetch(url, {
475
+ method: 'POST',
476
+ headers: { 'content-type': 'application/json', ...headers },
477
+ body: JSON.stringify(queryBody),
478
+ });
479
+
480
+ if (!res.ok) {
481
+ const errorData = await res.json();
482
+ console.error(`HTTP error! Status: ${res.status}, Details: ${JSON.stringify(errorData)}`);
483
+ if (errorData.hasOwnProperty('error')) {
484
+ return new DocumentsSnapshot([]);
485
+ }
486
+ }
487
+
488
+ const json = await toJsonOrThrow(res);
489
+
490
+ // Map each document to a DocumentSnapshot
491
+ const docs = (json.data || []).map((doc) => {
492
+ const data = deserializeRefs(this.collectionRef.db, doc.data || doc || {});
493
+ return new DocumentSnapshot(doc.id, data, `${this.collectionRef.fullPath}/${doc.id}`, this.collectionRef.db);
494
+ });
495
+ return new DocumentsSnapshot(docs);
496
+ }
497
+
498
+ onSnapshot(callback, errorCallback) {
499
+ const self = this;
500
+
501
+ const wsUrl = this.collectionRef.db.baseUrl
502
+ .replace(/^https/, 'wss')
503
+ .replace(/^http/, 'ws')
504
+ + `/${this.collectionRef.fullPath}/stream`;
505
+
506
+ //console.log(`Requesting a new web socket connection`, wsUrl);
507
+ const ws = new WebSocket(wsUrl);
508
+
509
+ ws.onopen = (event) => {
510
+ //console.log('ws.onopen', event);
511
+ const queryBody = this.build();
512
+ //console.log('Sending query...', queryBody);
513
+ ws.send(JSON.stringify(queryBody)); // send query definition once
514
+ //console.log('Sent query success');
515
+ };
516
+
517
+ ws.onmessage = (event) => {
518
+ //console.log('ws.onmessage');
519
+ try {
520
+ const msg = JSON.parse(event.data);
521
+ if (msg.type === 'init' || msg.type === 'change') {
522
+ let data = msg.data;
523
+ if (!Array.isArray(msg.data)) {
524
+ // nested data found, meaning we have op and id as well
525
+ data = [msg.data];
526
+ }
527
+ const docs = (data || []).map(doc => {
528
+ const data = deserializeRefs(this.collectionRef.db, doc.data || doc);
529
+ if (msg.data.hasOwnProperty('op')) {
530
+ data._data = msg.data;
531
+ }
532
+ return new DocumentSnapshot(
533
+ doc.id,
534
+ data,
535
+ `${this.collectionRef.fullPath}/${doc.id}`,
536
+ this.collectionRef.db
537
+ );
538
+ });
539
+ callback(new QuerySnapshot(docs), msg.type);
540
+ } else if (msg.type === 'error') {
541
+ errorCallback(msg);
542
+ }
543
+ } catch (err) {
544
+ if (typeof errorCallback === 'function') errorCallback(err);
545
+ else console.error('onSnapshot parsing error:', err);
546
+ }
547
+ };
548
+
549
+ ws.onerror = (err) => {
550
+ //console.log('ws.onerror');
551
+ if (typeof errorCallback === 'function') errorCallback(err);
552
+ else console.error('onSnapshot websocket error:', err);
553
+ };
554
+
555
+ ws.onclose = event => {
556
+ //console.log('ws.onclose', event);
557
+ };
558
+
559
+ return () => ws.close(); // return unsubscribe function
560
+ }
561
+
562
+ mergeFrom(other) {
563
+ this._filters.push(...(other._filters || []));
564
+ this._order.push(...(other._order || []));
565
+ if (other._limit) this._limit = other._limit;
566
+ if (other._offset) this._offset = other._offset;
567
+ if (other._startAt) this._startAt = other._startAt;
568
+ if (other._startAfter) this._startAfter = other._startAfter;
569
+ if (other._endAt) this._endAt = other._endAt;
570
+ if (other._endBefore) this._endBefore = other._endBefore;
571
+ return this;
572
+ }
573
+ }
574
+
575
+ /* Basic helpers similar to Firestore types */
576
+
577
+ function isIsoDateString(v) {
578
+ return (
579
+ typeof v === 'string' &&
580
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(v)
581
+ );
582
+ }
583
+
584
+ export class Timestamp {
585
+ constructor(seconds, nanoseconds) {
586
+ this._type = 'timestamp';
587
+ this.seconds = seconds;
588
+ this.nanoseconds = nanoseconds;
589
+ }
590
+
591
+ /** Create from JS Date */
592
+ static fromDate(date) {
593
+ const millis = date.getTime();
594
+ const seconds = Math.floor(millis / 1000);
595
+ const nanoseconds = (millis % 1000) * 1e6;
596
+ return new Timestamp(seconds, nanoseconds);
597
+ }
598
+
599
+ /** Now */
600
+ static now() {
601
+ return Timestamp.fromDate(new Date());
602
+ }
603
+
604
+ /** Create from Firestore-style seconds + nanos */
605
+ static fromSeconds(seconds, nanoseconds = 0) {
606
+ return new Timestamp(seconds, nanoseconds);
607
+ }
608
+
609
+ /** Create from Postgres ISO datetime string */
610
+ static fromPostgres(isoString) {
611
+ const date = new Date(isoString);
612
+
613
+ if (isNaN(date.getTime())) {
614
+ throw new Error("Invalid Postgres ISO datetime: " + isoString);
615
+ }
616
+
617
+ // Parse fractional seconds manually (Postgres can include microseconds)
618
+ const match = isoString.match(/\.(\d+)(?=Z|[+-]\d\d:?\d\d$)/);
619
+ let nanos = 0;
620
+
621
+ if (match) {
622
+ let fractional = match[1]; // e.g., "789123"
623
+ if (fractional.length > 9) {
624
+ fractional = fractional.slice(0, 9); // trim to nanoseconds
625
+ }
626
+ nanos = parseInt((fractional + "000000000").slice(0, 9), 10);
627
+ }
628
+
629
+ const seconds = Math.floor(date.getTime() / 1000);
630
+
631
+ return new Timestamp(seconds, nanos);
632
+ }
633
+
634
+ /** Convert back to JS Date */
635
+ toDate() {
636
+ return new Date(this.seconds * 1000 + Math.floor(this.nanoseconds / 1e6));
637
+ }
638
+
639
+ /** Milliseconds since epoch */
640
+ toMillis() {
641
+ return this.seconds * 1000 + Math.floor(this.nanoseconds / 1e6);
642
+ }
643
+
644
+ /** ISO string */
645
+ toString() {
646
+ return this.toDate().toISOString();
647
+ }
648
+ }
649
+
650
+ export const FieldValue = {
651
+ increment: (by = 1) => ({ _op: 'increment', by }),
652
+ serverTimestamp: () => ({ _op: 'serverTimestamp' }),
653
+ };
654
+
655
+ export const FieldPath = (path) => ({ _fieldPath: path });
656
+
657
+ export const documentId = () => "__id";