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
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ [<img src="https://i.imgur.com/oRFWBv7.png" height="80px" />](https://buymeacoffee.com/umrashrf)
2
+
3
+ ![Home Page Screenshot](assets/img/HomePageScreenshot.png)
4
+
5
+ <img alt="Node.js" src="https://github.com/umrashrf/postbase/raw/main/assets/img/node.js_logo.png?raw=true" height="100"> <img alt="Express.js" src="https://github.com/umrashrf/postbase/raw/main/assets/img/expresjs.png?raw=true" height="100"> <img alt="PostgreSQL" src="https://github.com/umrashrf/postbase/raw/main/assets/img/postgres.png?raw=true" height="100"> <img alt="Better-Auth" src="https://github.com/umrashrf/postbase/raw/main/assets/img/better-auth-logo-light.4b03f444.png?raw=true" height="100"> <img alt="Let's Encrypt" src="https://github.com/umrashrf/postbase/raw/main/assets/img/letsencrypt-logo-horizontal.png?raw=true" height="100">
6
+
7
+ # Postbase
8
+
9
+ Drop-in replacement for Firebase, production grade, open source, localhost first and self-hosted using Node.js, Express.js, BetterAuth and PostgreSQL (JSONB)
10
+
11
+ Firebase 💔 | Supabase 💔 | Postbase ❤️
12
+
13
+ Demo Preact app is included !
14
+
15
+ ## Features
16
+
17
+ ### Authentication Features
18
+
19
+ - [x] Sign Up ➕👤
20
+ - [x] Sign In 🔑
21
+ - [x] Sign in with Google/Facebook/Apple etc. ➕👤
22
+ - [x] Forgot Password ❓🔐
23
+ - [x] Reset Password ♻️🔐
24
+ - [x] Email Verification Email ✉️✔️
25
+ - [x] Phone Verification Codes 📱✔️
26
+ - [x] Delete User 👤❌
27
+
28
+ Special thanks to [@better-auth/better-auth](https://github.com/better-auth/better-auth)
29
+
30
+ ### Database Features
31
+
32
+ - [x] NoSQL Document Storage 🗄️
33
+ - [x] Collections 📁
34
+ - [x] Query functions 🔍
35
+ - [x] CRUD Functions 🛠️
36
+ - [x] Security Rules 🛡️
37
+ - [x] Database Migrations 🛢️ → 🛢️
38
+
39
+ ### File Upload / Storage
40
+
41
+ - [x] File Upload (https) 📄⬆️
42
+ - [x] File Serving (https) 📄⬇️
43
+ - [x] Security Rules 🛡️
44
+
45
+ ### Admin & System
46
+
47
+ - [x] Admin SDK 👑🗄️
48
+ - [x] Nginx Config 🧱
49
+ - [x] Systemd Service ⚙️
50
+ - [x] Git Push Deployment ⬆️🐙
51
+
52
+ ## Disclaimer !!!
53
+
54
+ Brand new project launched 02 Nov 2025, this is boiler plate but working! Expect heavy changes coming every few hours until stable
55
+
56
+ The project started with the help of LLMs but over time heavy use of LLMs stopped due to large uncontrolled changes.
57
+
58
+ ## Getting Started
59
+
60
+ To create a new project with Postbase, all you have to do is clone this repo.
61
+
62
+ ```
63
+ git clone https://github.com/umrashrf/postbase.git
64
+ ```
65
+
66
+ then start backend and frontend servers and modify as needed!
67
+
68
+ Both backend/ and frontend/ folders have their own README.md
69
+
70
+ ## Vision
71
+
72
+ Make a drop-in replacement library for Firebase (keep same naming convention). So users can switch between Firebase and Postbase as they please.
73
+
74
+ ## Docs
75
+
76
+ ### Getting Started
77
+
78
+ ```javascript
79
+ import { initializeApp } from "../lib/postbase/compat/firebase/app";
80
+ import { getFirestore } from "../lib/postbase/compat/firebase/firestore/lite";
81
+ import { getStorage } from "../lib/postbase/compat/firebase/storage";
82
+ import { getDatabase } from "../lib/postbase/compat/firebase/database";
83
+
84
+ const firebaseConfig = {
85
+ baseUrl: import.meta.env.VITE_API_BASE,
86
+ };
87
+
88
+ const app = initializeApp(firebaseConfig);
89
+
90
+ export const db = getFirestore(app);
91
+ export const storage = getStorage(app);
92
+ export const rtdbClient = getDatabase(app);
93
+ ```
94
+
95
+ ### Authentication (Firebase Like API)
96
+
97
+ #### Sign Up
98
+
99
+ ```javascript
100
+ import { getAuth, createUserWithEmailAndPassword } from "./auth";
101
+
102
+ const auth = getAuth();
103
+
104
+ const userCredential = await createUserWithEmailAndPassword(auth, 'email', 'password');
105
+ ```
106
+
107
+ #### Sign In
108
+
109
+ ```javascript
110
+ import { getAuth, signInWithEmailAndPassword } from "./auth";
111
+
112
+ const auth = getAuth();
113
+
114
+ const userCredential = await signInWithEmailAndPassword(auth, 'email', 'password');
115
+ ```
116
+
117
+ #### auth.onAuthStateChanged, auth.currentUser and auth.currentUser.getIdToken()
118
+
119
+ ```javascript
120
+ import { auth } from './auth';
121
+
122
+ auth.onAuthStateChanged(user => {
123
+ // user
124
+ auth.currentUser === user // true
125
+ });
126
+
127
+ const token = auth.currentUser.getIdToken();
128
+ // token for API authentication and rules engine
129
+ ```
130
+
131
+ Tip: Add this link https://email.riamu.io to your Sign Up page for your users to get a free email address
132
+
133
+ ### Document Storage (Firestore Like API)
134
+
135
+ #### Collections, get/set/where/orderBy/limit/delete
136
+
137
+ ```javascript
138
+ import { db } from "./postbase";
139
+
140
+ const data = await db.collection('users').doc('docId').get();
141
+ // getDoc(collection(db, 'users'), 'docId')
142
+
143
+ await db.collection('users').set({ name: "Umair" }, { merge: true });
144
+ // getDoc(collection(db, 'users'), 'docId')
145
+
146
+ const reference = db.collection('users')
147
+ .where('name', '==', 'Umair')
148
+ .orderBy('createdAt')
149
+ .limit(5);
150
+ // const reference = getDocs(query(collection(db, 'users'), where('name', '==', 'Umair'), orderBy('createdAt')))
151
+
152
+ const docs = await reference.get();
153
+ // const docs = await getDocs(reference);
154
+
155
+ reference.onSnapshot(docs => {
156
+ // use docs
157
+ });
158
+ // onSnapshot(reference, docs => {
159
+ //
160
+ // })
161
+ ```
162
+
163
+ #### Admin Client
164
+
165
+ ```javascript
166
+ import { createAdminClient } from './lib/postbase/compat/admin.js';
167
+ import { authClient } from './admin/auth.js';
168
+
169
+ const admin = createAdminClient({ authClient });
170
+
171
+ const user = await admin.auth().getUser(userId);
172
+
173
+ const doc = await admin.firestore().collection('collection').doc('docId').get();
174
+ ```
175
+
176
+ ### Todo
177
+ - [ ] Firebase Functions Replacement (Backend API can be used for now)
178
+
179
+ Important functions to replicate:
180
+
181
+ ```
182
+ # https://firebase.google.com/docs/functions/schedule-functions
183
+ const { onSchedule } = require("firebase-functions/scheduler");
184
+
185
+ # https://firebase.google.com/docs/functions/callable
186
+ const { onCall } = require("firebase-functions/https");
187
+
188
+ # https://firebase.google.com/docs/functions/get-started
189
+ const { onRequest } = require("firebase-functions/https");
190
+ ```
191
+
192
+ - [ ] Firebase Storage Replacement (Support S3 and other backend)
193
+
194
+ ### In Progress
195
+ - [ ] Testing
196
+
197
+ ### Done
198
+ - [x] Firebase Authentication Replacement
199
+ - [x] Firebase Firestore Replacement
200
+ - [x] Firebase Storage Replacement (Filebased Only)
201
+ - [x] Firebase Storage Replacement (HTTPS Based Upload)
202
+
203
+ ## Star History
204
+
205
+ <a href="https://star-history.com/#umrashrf/postbase&Date">
206
+ <picture>
207
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=umrashrf/postbase&type=Date&theme=dark" />
208
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=umrashrf/postbase&type=Date" />
209
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=umrashrf/postbase&type=Date" />
210
+ </picture>
211
+ </a>
package/admin/404.html ADDED
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Page Not Found</title>
7
+
8
+ <style media="screen">
9
+ body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
10
+ #message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
11
+ #message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
12
+ #message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
13
+ #message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
14
+ #message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
15
+ #message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
16
+ #message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
17
+ #load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
18
+ @media (max-width: 600px) {
19
+ body, #message { margin-top: 0; background: white; box-shadow: none; }
20
+ body { border-top: 16px solid #ffa100; }
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div id="message">
26
+ <h2>404</h2>
27
+ <h1>Page Not Found</h1>
28
+ <p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
29
+ <h3>Why am I seeing this?</h3>
30
+ <p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
31
+ </div>
32
+ </body>
33
+ </html>
@@ -0,0 +1,21 @@
1
+ # Admin Panel
2
+
3
+ This directory contains working but incomplete version of admin panel. It's work in progress. PRs are welcome!
4
+
5
+ ![Admin Panel Demo Screenshot](public/assets/img/admin-ui.png)
6
+
7
+ ## Install
8
+
9
+ ```
10
+ cd admin
11
+ cp template.env .env
12
+ npm insall
13
+ ```
14
+
15
+ Make sure your backend is running and double check admin/.env file is accurate.
16
+
17
+ ## Run
18
+
19
+ ```
20
+ npm run dev
21
+ ```
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>Postbase Demo</title>
8
+ </head>
9
+
10
+ <body>
11
+ <div id="app"></div>
12
+ <script type="module" src="/src/main.jsx"></script>
13
+ </body>
14
+
15
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "noEmit": true,
7
+ "allowJs": true,
8
+ "checkJs": true,
9
+
10
+ /* Preact Config */
11
+ "jsx": "react-jsx",
12
+ "jsxImportSource": "preact",
13
+ "skipLibCheck": true,
14
+ "paths": {
15
+ "react": ["./node_modules/preact/compat/"],
16
+ "react-dom": ["./node_modules/preact/compat/"]
17
+ }
18
+ },
19
+ "include": ["node_modules/vite/client.d.ts", "**/*"]
20
+ }
@@ -0,0 +1,222 @@
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.addDoc({ title: 'hi' });
7
+ // const doc = await posts.doc('123').get();
8
+
9
+ function toJsonOrThrow(res) {
10
+ if (!res.ok) {
11
+ return res.json().then(j => { throw j; });
12
+ }
13
+ return res.json();
14
+ }
15
+
16
+ export function getDB({
17
+ baseUrl = '/api',
18
+ defaultHeaders = {},
19
+ getAuthToken = null, // 👈 optional async token resolver
20
+ } = {}) {
21
+ return new Database(baseUrl.replace(/\/$/, ''), defaultHeaders, getAuthToken);
22
+ }
23
+
24
+ class Database {
25
+ constructor(baseUrl, defaultHeaders, getAuthToken) {
26
+ this.baseUrl = baseUrl;
27
+ this.defaultHeaders = defaultHeaders;
28
+ this.getAuthToken = getAuthToken;
29
+ }
30
+
31
+ collection(name) {
32
+ return new CollectionReference(this, name);
33
+ }
34
+
35
+ async getHeaders() {
36
+ const headers = { ...this.defaultHeaders };
37
+ if (typeof this.getAuthToken === 'function') {
38
+ try {
39
+ const token = await this.getAuthToken();
40
+ if (token) headers['Authorization'] = `Bearer ${token}`;
41
+ } catch (err) {
42
+ console.warn('getAuthToken failed', err);
43
+ }
44
+ }
45
+ return headers;
46
+ }
47
+ }
48
+
49
+ class CollectionReference {
50
+ constructor(db, name, parentPath = null) {
51
+ this.db = db;
52
+ this.name = name;
53
+ this.parentPath = parentPath; // e.g., "users/u1"
54
+ }
55
+
56
+ /** Build full collection path */
57
+ get fullPath() {
58
+ return this.parentPath ? `${this.parentPath}/${this.name}` : this.name;
59
+ }
60
+
61
+ /** Return a DocumentReference inside this collection */
62
+ doc(id) {
63
+ return new DocumentReference(this.db, this.name, id, this.parentPath);
64
+ }
65
+
66
+ /** Allow chaining subcollections under this collection — for convenience */
67
+ collection(subName) {
68
+ // Enables chaining like: db.collection('orgs').collection('users')
69
+ // Useful for root-level logical grouping (not subcollections of docs)
70
+ return new CollectionReference(this.db, subName, this.fullPath);
71
+ }
72
+
73
+ // POST /:collection
74
+ async addDoc(data) {
75
+ const url = `${this.db.baseUrl}/${encodeURIComponent(this.fullPath)}`;
76
+ const headers = await this.db.getHeaders();
77
+ const res = await fetch(url, {
78
+ method: 'POST',
79
+ headers: { 'content-type': 'application/json', ...headers },
80
+ body: JSON.stringify(data)
81
+ });
82
+ const json = await toJsonOrThrow(res);
83
+ return json.data;
84
+ }
85
+
86
+ // POST /:collection/query
87
+ async getDocs(query = {}) {
88
+ const url = `${this.db.baseUrl}/${encodeURIComponent(this.fullPath)}/query`;
89
+ const headers = await this.db.getHeaders();
90
+ const res = await fetch(url, {
91
+ method: 'POST',
92
+ headers: { 'content-type': 'application/json', ...headers },
93
+ body: JSON.stringify(query)
94
+ });
95
+ const json = await toJsonOrThrow(res);
96
+ return json.data || [];
97
+ }
98
+
99
+ where(field, op, value) {
100
+ return new QueryBuilder(this.fullPath).where(field, op, value);
101
+ }
102
+ }
103
+
104
+ class DocumentReference {
105
+ constructor(db, collectionName, id, parentPath = null) {
106
+ this.db = db;
107
+ this.collectionName = collectionName;
108
+ this.id = id;
109
+ this.parentPath = parentPath;
110
+ }
111
+
112
+ /** Full document path, e.g. "users/u1/posts/p2" */
113
+ get fullPath() {
114
+ const base = this.parentPath ? `${this.parentPath}/${this.collectionName}` : this.collectionName;
115
+ return `${base}/${this.id}`;
116
+ }
117
+
118
+ /** Allow chaining subcollections under this document */
119
+ collection(subName) {
120
+ return new CollectionReference(this.db, subName, this.fullPath);
121
+ }
122
+
123
+ async get() {
124
+ const url = `${this.db.baseUrl}/${encodeURIComponent(this.fullPath)}`;
125
+ const headers = await this.db.getHeaders();
126
+ const res = await fetch(url, { headers });
127
+ const json = await toJsonOrThrow(res);
128
+ return json.data;
129
+ }
130
+
131
+ async set(data) {
132
+ const url = `${this.db.baseUrl}/${encodeURIComponent(this.fullPath)}`;
133
+ const headers = await this.db.getHeaders();
134
+ const res = await fetch(url, {
135
+ method: 'PUT',
136
+ headers: { 'content-type': 'application/json', ...headers },
137
+ body: JSON.stringify(data),
138
+ });
139
+ const json = await toJsonOrThrow(res);
140
+ return json.data;
141
+ }
142
+
143
+ async update(data) {
144
+ const url = `${this.db.baseUrl}/${encodeURIComponent(this.fullPath)}`;
145
+ const headers = await this.db.getHeaders();
146
+ const res = await fetch(url, {
147
+ method: 'PATCH',
148
+ headers: { 'content-type': 'application/json', ...headers },
149
+ body: JSON.stringify(data),
150
+ });
151
+ const json = await toJsonOrThrow(res);
152
+ return json.data;
153
+ }
154
+
155
+ async delete() {
156
+ const url = `${this.db.baseUrl}/${encodeURIComponent(this.fullPath)}`;
157
+ const headers = await this.db.getHeaders();
158
+ const res = await fetch(url, { method: 'DELETE', headers });
159
+ const json = await toJsonOrThrow(res);
160
+ return json.data;
161
+ }
162
+ }
163
+
164
+ /* Query builder helpers */
165
+ export function query(collectionRef, ...clauses) {
166
+ // returns a structured query object for backend: { filters: [...], order: [...], limit, offset }
167
+ const q = new QueryBuilder(collectionRef);
168
+ for (const c of clauses) {
169
+ if (c instanceof QueryBuilder) q.mergeFrom(c);
170
+ }
171
+ return q;
172
+ }
173
+
174
+ class QueryBuilder {
175
+ constructor(collectionNameOrRef) {
176
+ this.collection = (collectionNameOrRef && collectionNameOrRef.name) || collectionNameOrRef;
177
+ this._filters = [];
178
+ this._order = [];
179
+ this._limit = undefined;
180
+ this._offset = undefined;
181
+ }
182
+ where(field, op, value) {
183
+ this._filters.push({ field, op, value });
184
+ return this;
185
+ }
186
+ orderBy(field, dir = 'asc') {
187
+ this._order.push({ field, dir });
188
+ return this;
189
+ }
190
+ limit(n) { this._limit = n; return this; }
191
+ offset(n) { this._offset = n; return this; }
192
+ build() {
193
+ const out = {};
194
+ if (this._filters.length) out.filters = this._filters;
195
+ if (this._order.length) out.order = this._order;
196
+ if (typeof this._limit !== 'undefined') out.limit = this._limit;
197
+ if (typeof this._offset !== 'undefined') out.offset = this._offset;
198
+ return out;
199
+ }
200
+ mergeFrom(other) {
201
+ this._filters.push(...(other._filters || []));
202
+ this._order.push(...(other._order || []));
203
+ if (other._limit) this._limit = other._limit;
204
+ if (other._offset) this._offset = other._offset;
205
+ return this;
206
+ }
207
+ }
208
+
209
+ /* Basic helpers similar to Firestore types */
210
+ export const Timestamp = {
211
+ now: () => ({ _type: 'timestamp', iso: new Date().toISOString() }),
212
+ fromDate: (d) => ({ _type: 'timestamp', iso: d.toISOString() })
213
+ };
214
+
215
+ export const FieldValue = {
216
+ increment: (by = 1) => ({ _op: 'increment', by }),
217
+ serverTimestamp: () => ({ _op: 'serverTimestamp' }),
218
+ };
219
+
220
+ export const FieldPath = (path) => ({ _fieldPath: path });
221
+
222
+ export const documentId = (id) => ({ _documentId: id });