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.
- package/.github/workflows/test.yml +74 -0
- package/CLA.md +60 -0
- package/CONTRIBUTORS.md +35 -0
- package/LICENSE +661 -0
- package/README.md +211 -0
- package/admin/404.html +33 -0
- package/admin/README.md +21 -0
- package/admin/index.html +15 -0
- package/admin/jsconfig.json +20 -0
- package/admin/lib/postbase.js +222 -0
- package/admin/package-lock.json +3746 -0
- package/admin/package.json +27 -0
- package/admin/public/assets/img/admin-ui.png +0 -0
- package/admin/public/assets/img/blank-profile-picture-960_720.webp +0 -0
- package/admin/public/assets/img/chart-active-users.png +0 -0
- package/admin/public/assets/img/icon-transparent.png +0 -0
- package/admin/src/App.jsx +48 -0
- package/admin/src/auth.js +11 -0
- package/admin/src/common/formatDateTime.js +18 -0
- package/admin/src/components/AuthPanel.jsx +88 -0
- package/admin/src/components/Header.jsx +67 -0
- package/admin/src/main.jsx +6 -0
- package/admin/src/pages/Dashboard.jsx +24 -0
- package/admin/src/pages/Home.jsx +52 -0
- package/admin/src/pages/Login.jsx +10 -0
- package/admin/src/pages/authentication/Users.jsx +199 -0
- package/admin/src/pages/firestore/Database.jsx +29 -0
- package/admin/src/pages/storage/files.jsx +29 -0
- package/admin/src/postbase.js +15 -0
- package/admin/src/styles.css +3 -0
- package/admin/tailwind.config.cjs +11 -0
- package/admin/template.env +2 -0
- package/admin/vite.config.js +21 -0
- package/assets/img/HomePageScreenshot.png +0 -0
- package/assets/img/better-auth-logo-dark.136b122f.png +0 -0
- package/assets/img/better-auth-logo-light.4b03f444.png +0 -0
- package/assets/img/expresjs.png +0 -0
- package/assets/img/icon-transparent.png +0 -0
- package/assets/img/icon.png +0 -0
- package/assets/img/letsencrypt-logo-horizontal.png +0 -0
- package/assets/img/logo.png +0 -0
- package/assets/img/node.js_logo.png +0 -0
- package/assets/img/nodejsLight.svg +39 -0
- package/assets/img/postgres.png +0 -0
- package/backend/README.md +49 -0
- package/backend/admin/auth.js +9 -0
- package/backend/app.js +68 -0
- package/backend/auth.js +92 -0
- package/backend/env.js +12 -0
- package/backend/lib/postbase/adminClient.js +520 -0
- package/backend/lib/postbase/compat/admin.js +44 -0
- package/backend/lib/postbase/db.js +17 -0
- package/backend/lib/postbase/genericRouter.js +603 -0
- package/backend/lib/postbase/local-storage.js +56 -0
- package/backend/lib/postbase/metadataCache.js +32 -0
- package/backend/lib/postbase/middlewares/auth.js +57 -0
- package/backend/lib/postbase/migrations/1765239687559_rtdb-nodes.js +93 -0
- package/backend/lib/postbase/package-lock.json +5873 -0
- package/backend/lib/postbase/package.json +19 -0
- package/backend/lib/postbase/rtdb/router.js +190 -0
- package/backend/lib/postbase/rtdb/rulesEngine.js +63 -0
- package/backend/lib/postbase/rtdb/ws.js +84 -0
- package/backend/lib/postbase/rulesEngine.js +62 -0
- package/backend/lib/postbase/storage.js +130 -0
- package/backend/lib/postbase/tests/README.md +22 -0
- package/backend/lib/postbase/tests/db.js +9 -0
- package/backend/lib/postbase/tests/rtdb.rest.test.js +46 -0
- package/backend/lib/postbase/tests/rtdb.ws.test.js +113 -0
- package/backend/lib/postbase/tests/rules.js +26 -0
- package/backend/lib/postbase/tests/testServer.js +46 -0
- package/backend/lib/postbase/websocket.js +131 -0
- package/backend/local.js +6 -0
- package/backend/main.js +20 -0
- package/backend/middlewares/auth_middleware.js +10 -0
- package/backend/migrations/1762137399366-init.sql +98 -0
- package/backend/migrations/1762137399367_init_jsonb_schema.js +68 -0
- package/backend/migrations/1762149999999_enable_realtime_changes.js +48 -0
- package/backend/migrations/1765224247654_rtdb-nodes.js +93 -0
- package/backend/package-lock.json +2374 -0
- package/backend/package.json +27 -0
- package/backend/postbase_db_rules.js +128 -0
- package/backend/postbase_rtdb_rules.js +27 -0
- package/backend/postbase_storage_rules.js +45 -0
- package/backend/template.env +10 -0
- package/backend-systemd/README.md +39 -0
- package/backend-systemd/your_website.com.service +12 -0
- package/frontend/404.html +33 -0
- package/frontend/README.md +25 -0
- package/frontend/index.html +15 -0
- package/frontend/jsconfig.json +20 -0
- package/frontend/lib/postbase/auth.js +132 -0
- package/frontend/lib/postbase/compat/firebase/app.js +3 -0
- package/frontend/lib/postbase/compat/firebase/auth.js +115 -0
- package/frontend/lib/postbase/compat/firebase/database.js +11 -0
- package/frontend/lib/postbase/compat/firebase/firestore/lite.js +61 -0
- package/frontend/lib/postbase/compat/firebase/storage.js +10 -0
- package/frontend/lib/postbase/db.js +657 -0
- package/frontend/lib/postbase/package-lock.json +6284 -0
- package/frontend/lib/postbase/package.json +17 -0
- package/frontend/lib/postbase/rtdb.js +108 -0
- package/frontend/lib/postbase/storage.js +293 -0
- package/frontend/lib/postbase/tests/rtdb.client.test.js +88 -0
- package/frontend/lib/postbase/tests/waitFor.js +13 -0
- package/frontend/lib/postbase/utils.js +1 -0
- package/frontend/package-lock.json +2977 -0
- package/frontend/package.json +24 -0
- package/frontend/src/App.jsx +38 -0
- package/frontend/src/auth.js +52 -0
- package/frontend/src/components/AuthPanel.jsx +85 -0
- package/frontend/src/components/Header.jsx +54 -0
- package/frontend/src/main.jsx +5 -0
- package/frontend/src/pages/Dashboard.jsx +24 -0
- package/frontend/src/pages/Home.jsx +178 -0
- package/frontend/src/pages/Login.jsx +10 -0
- package/frontend/src/postbase.js +14 -0
- package/frontend/src/styles.css +1 -0
- package/frontend/tailwind.config.cjs +11 -0
- package/frontend/template.env +2 -0
- package/frontend/vite.config.js +18 -0
- package/git/hooks/README.md +31 -0
- package/git/hooks/post-receive +26 -0
- package/nginx/README.md +84 -0
- package/nginx/apt/www.your_website.com.conf +80 -0
- package/nginx/homebrew/www.your_website.com.conf +80 -0
- package/nginx/letsencrypt/README +14 -0
- 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
|
+

|
|
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>
|
package/admin/README.md
ADDED
|
@@ -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
|
+

|
|
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
|
+
```
|
package/admin/index.html
ADDED
|
@@ -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 });
|