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
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { runQuery } from './db.js';
|
|
3
|
+
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/* Firestore-like Helpers */
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
|
|
8
|
+
export class Timestamp {
|
|
9
|
+
constructor(seconds, nanoseconds) {
|
|
10
|
+
this._type = 'timestamp';
|
|
11
|
+
this.seconds = seconds;
|
|
12
|
+
this.nanoseconds = nanoseconds;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Create from JS Date */
|
|
16
|
+
static fromDate(date) {
|
|
17
|
+
const millis = date.getTime();
|
|
18
|
+
const seconds = Math.floor(millis / 1000);
|
|
19
|
+
const nanoseconds = (millis % 1000) * 1e6;
|
|
20
|
+
return new Timestamp(seconds, nanoseconds);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Now */
|
|
24
|
+
static now() {
|
|
25
|
+
return Timestamp.fromDate(new Date());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Create from Firestore-style seconds + nanos */
|
|
29
|
+
static fromSeconds(seconds, nanoseconds = 0) {
|
|
30
|
+
return new Timestamp(seconds, nanoseconds);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Create from Postgres ISO datetime string */
|
|
34
|
+
static fromPostgres(isoString) {
|
|
35
|
+
const date = new Date(isoString);
|
|
36
|
+
|
|
37
|
+
if (isNaN(date.getTime())) {
|
|
38
|
+
throw new Error("Invalid Postgres ISO datetime: " + isoString);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parse fractional seconds manually (Postgres can include microseconds)
|
|
42
|
+
const match = isoString.match(/\.(\d+)(?=Z|[+-]\d\d:?\d\d$)/);
|
|
43
|
+
let nanos = 0;
|
|
44
|
+
|
|
45
|
+
if (match) {
|
|
46
|
+
let fractional = match[1]; // e.g., "789123"
|
|
47
|
+
if (fractional.length > 9) {
|
|
48
|
+
fractional = fractional.slice(0, 9); // trim to nanoseconds
|
|
49
|
+
}
|
|
50
|
+
nanos = parseInt((fractional + "000000000").slice(0, 9), 10);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const seconds = Math.floor(date.getTime() / 1000);
|
|
54
|
+
|
|
55
|
+
return new Timestamp(seconds, nanos);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Convert back to JS Date */
|
|
59
|
+
toDate() {
|
|
60
|
+
return new Date(this.seconds * 1000 + Math.floor(this.nanoseconds / 1e6));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Milliseconds since epoch */
|
|
64
|
+
toMillis() {
|
|
65
|
+
return this.seconds * 1000 + Math.floor(this.nanoseconds / 1e6);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** ISO string */
|
|
69
|
+
toString() {
|
|
70
|
+
return this.toDate().toISOString();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const FieldValue = {
|
|
75
|
+
increment: (by = 1) => ({ _op: 'increment', by }),
|
|
76
|
+
serverTimestamp: () => ({ _op: 'serverTimestamp' }),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const FieldPath = (path) => ({ _fieldPath: path });
|
|
80
|
+
export const documentId = () => "__id";
|
|
81
|
+
|
|
82
|
+
/* ======================================================================= */
|
|
83
|
+
/* Admin Client Factory */
|
|
84
|
+
/* ======================================================================= */
|
|
85
|
+
|
|
86
|
+
export function makePostbaseAdminClient({ pool }) {
|
|
87
|
+
let self = null;
|
|
88
|
+
|
|
89
|
+
const ALLOWED_OPS = new Set([
|
|
90
|
+
'==',
|
|
91
|
+
'!=',
|
|
92
|
+
'<',
|
|
93
|
+
'<=',
|
|
94
|
+
'>',
|
|
95
|
+
'>=',
|
|
96
|
+
'LIKE',
|
|
97
|
+
'ILIKE',
|
|
98
|
+
'IN',
|
|
99
|
+
'array-contains', // only supports strings "large" in ["red", "blue", "large"]
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// === WHERE builder ===
|
|
103
|
+
function buildWhere(filters = []) {
|
|
104
|
+
const where = [];
|
|
105
|
+
const params = [];
|
|
106
|
+
let i = 1;
|
|
107
|
+
|
|
108
|
+
for (const f of filters) {
|
|
109
|
+
const { field, op, value } = f;
|
|
110
|
+
const sqlOp = op === "==" ? "=" : op;
|
|
111
|
+
|
|
112
|
+
if (field === "__id") {
|
|
113
|
+
params.push(value);
|
|
114
|
+
where.push(`id ${sqlOp} $${i++}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
else if (op === "IN") {
|
|
119
|
+
if (!Array.isArray(value)) throw new Error("IN requires array");
|
|
120
|
+
const ph = value.map(() => `$${i++}`).join(",");
|
|
121
|
+
params.push(...value);
|
|
122
|
+
where.push(`data->>'${field}' IN (${ph})`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
else if (op === "array-contains") {
|
|
127
|
+
if (value && value._type === "ref") {
|
|
128
|
+
params.push(JSON.stringify([value]));
|
|
129
|
+
where.push(`data->'${field}' @> $${i++}::jsonb`);
|
|
130
|
+
} else {
|
|
131
|
+
params.push(value);
|
|
132
|
+
where.push(`(data->'${field}') ? $${i++}`);
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
else if (isDocumentRef(value)) {
|
|
138
|
+
params.push(resolveReference(value));
|
|
139
|
+
where.push(`data->'${field}'->>'path' ${sqlOp} $${i++}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
else {
|
|
144
|
+
params.push(value);
|
|
145
|
+
where.push(`data->>'${field}' ${sqlOp} $${i++}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
whereSql: where.length ? "WHERE " + where.join(" AND ") : "",
|
|
152
|
+
params,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// === ORDER builder ===
|
|
157
|
+
function buildOrder(order = []) {
|
|
158
|
+
if (!order.length) return '';
|
|
159
|
+
const parts = order.map(({ field, dir }) =>
|
|
160
|
+
`data->>'${field}' ${dir?.toLowerCase() === 'desc' ? 'DESC' : 'ASC'}`
|
|
161
|
+
);
|
|
162
|
+
return parts.length ? `ORDER BY ${parts.join(', ')}` : '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// === Apply Firestore-like FieldValue logic ===
|
|
166
|
+
function applyFieldValues(target, updates) {
|
|
167
|
+
const now = new Date().toISOString();
|
|
168
|
+
const result = { ...target };
|
|
169
|
+
|
|
170
|
+
console.log('updates', updates);
|
|
171
|
+
|
|
172
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
173
|
+
if (!v) {
|
|
174
|
+
result[k] = v;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (v.hasOwnProperty('_op') && v._op && v._op === "increment") {
|
|
179
|
+
result[k] = Number(result[k] || 0) + v.by;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (v.hasOwnProperty('_op') && v._op && v._op === "serverTimestamp") {
|
|
184
|
+
result[k] = now;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (v instanceof Timestamp) {
|
|
189
|
+
result[k] = v.toString();
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (v.hasOwnProperty('_type') && v._type && v._type === "ref") {
|
|
194
|
+
result[k] = { _type: "ref", path: v.path };
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!v || typeof v !== "object") {
|
|
199
|
+
result[k] = v;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
result[k] = serializeForWrite(v);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* =================================================================== */
|
|
210
|
+
/* Snapshot classes */
|
|
211
|
+
/* =================================================================== */
|
|
212
|
+
|
|
213
|
+
class AdminDocumentSnapshot {
|
|
214
|
+
constructor(id, rawData, ref) {
|
|
215
|
+
this.id = id;
|
|
216
|
+
this._raw = rawData;
|
|
217
|
+
this._data = adminDeserialize(rawData);
|
|
218
|
+
this.ref = ref;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
get exists() {
|
|
222
|
+
return !!this._data;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
data() {
|
|
226
|
+
return this._data;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
raw() {
|
|
230
|
+
return this._raw;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class AdminQuerySnapshot {
|
|
235
|
+
constructor(docs) {
|
|
236
|
+
this.docs = docs;
|
|
237
|
+
}
|
|
238
|
+
forEach(fn) { this.docs.forEach(fn); }
|
|
239
|
+
get empty() { return this.docs.length === 0; }
|
|
240
|
+
get size() { return this.docs.length; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ------------------------------------------------------------------ */
|
|
244
|
+
/* Document Reference */
|
|
245
|
+
/* ------------------------------------------------------------------ */
|
|
246
|
+
class DocumentRef {
|
|
247
|
+
constructor(collectionName, id, parentPath = null) {
|
|
248
|
+
this.collectionName = collectionName;
|
|
249
|
+
this.id = id;
|
|
250
|
+
this.parentPath = parentPath; // e.g. "users/u1"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Build full document path (for chaining use only) */
|
|
254
|
+
get fullPath() {
|
|
255
|
+
const base = this.parentPath ? `${this.parentPath}/${this.collectionName}` : this.collectionName;
|
|
256
|
+
return `${base}/${this.id}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Allow chaining subcollections under this document */
|
|
260
|
+
collection(subName) {
|
|
261
|
+
return new CollectionRef(subName, this.fullPath);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async get(client = pool) {
|
|
265
|
+
const sql = `SELECT id, data FROM "${this.collectionName}" WHERE id = $1 LIMIT 1`;
|
|
266
|
+
console.log(`Executing: ${sql}`);
|
|
267
|
+
console.log(`with params ${this.id}`)
|
|
268
|
+
const result = await runQuery(client, sql, [this.id]);
|
|
269
|
+
|
|
270
|
+
if (!result.rowCount) {
|
|
271
|
+
return new AdminDocumentSnapshot(this.id, null, this);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const row = result.rows[0];
|
|
275
|
+
return new AdminDocumentSnapshot(row.id, row.data, this);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async set(data, opts = {}, client = pool) {
|
|
279
|
+
const existing = await this.get(client); // data has been deserialized
|
|
280
|
+
const base = opts.merge && existing.exists ? existing.raw() : {};
|
|
281
|
+
const finalData = applyFieldValues(base, serializeForWrite(data));
|
|
282
|
+
|
|
283
|
+
const sql = `
|
|
284
|
+
INSERT INTO "${this.collectionName}" (id, data)
|
|
285
|
+
VALUES ($1, $2)
|
|
286
|
+
ON CONFLICT (id)
|
|
287
|
+
DO UPDATE SET data = EXCLUDED.data, updated_at = now() at time zone 'UTC'
|
|
288
|
+
RETURNING id, data`;
|
|
289
|
+
const result = await runQuery(client, sql, [this.id, finalData]);
|
|
290
|
+
const row = result.rows[0];
|
|
291
|
+
return new AdminDocumentSnapshot(row.id, row.data, this);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async update(partial, client = pool) {
|
|
295
|
+
const existing = await this.get(client);
|
|
296
|
+
if (!existing.exists) throw new Error("Document does not exist");
|
|
297
|
+
const merged = applyFieldValues(existing.raw(), serializeForWrite(partial));
|
|
298
|
+
return await this.set(merged, { merge: false }, client);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async delete(client = pool) {
|
|
302
|
+
const sql = `DELETE FROM "${this.collectionName}" WHERE id=$1 RETURNING id`;
|
|
303
|
+
const r = await runQuery(client, sql, [this.id]);
|
|
304
|
+
return r.rowCount ? r.rows[0].id : null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ------------------------------------------------------------------ */
|
|
309
|
+
/* Collection Reference */
|
|
310
|
+
/* ------------------------------------------------------------------ */
|
|
311
|
+
class CollectionRef {
|
|
312
|
+
constructor(collectionName, parentPath = null) {
|
|
313
|
+
this.collectionName = collectionName;
|
|
314
|
+
this.parentPath = parentPath; // e.g. "users/u1"
|
|
315
|
+
this._filters = [];
|
|
316
|
+
this._order = [];
|
|
317
|
+
this._limit = null;
|
|
318
|
+
this._offset = null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Build full collection path (for chaining use only) */
|
|
322
|
+
get fullPath() {
|
|
323
|
+
return this.parentPath ? `${this.parentPath}/${this.collectionName}` : this.collectionName;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Return a DocumentRef in this collection */
|
|
327
|
+
doc(id) {
|
|
328
|
+
return new DocumentRef(this.collectionName, id, this.parentPath);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Allow chaining subcollections under this collection */
|
|
332
|
+
collection(subName) {
|
|
333
|
+
return new CollectionRef(subName, this.fullPath);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
where(field, op, value) {
|
|
337
|
+
if (value && typeof value === "object" && value.fullPath) {
|
|
338
|
+
// DocumentRef inbound (admin)
|
|
339
|
+
value = { _type: "ref", path: value.fullPath };
|
|
340
|
+
}
|
|
341
|
+
this._filters.push({ field, op, value });
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
orderBy(field, dir = "asc") {
|
|
346
|
+
this._order.push({ field, dir });
|
|
347
|
+
return this;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
limit(n) {
|
|
351
|
+
this._limit = n;
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
offset(n) {
|
|
356
|
+
this._offset = n;
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async add(data, client = pool) {
|
|
361
|
+
console.log('data before serializing', data);
|
|
362
|
+
console.log('data after serializing', serializeForWrite(data));
|
|
363
|
+
|
|
364
|
+
const id = randomUUID();
|
|
365
|
+
const prepared = applyFieldValues({}, serializeForWrite(data));
|
|
366
|
+
|
|
367
|
+
const sql = `INSERT INTO "${this.collectionName}" (id, data) VALUES ($1, $2) RETURNING id, data;`;
|
|
368
|
+
const r = await runQuery(client, sql, [id, prepared]);
|
|
369
|
+
const row = r.rows[0];
|
|
370
|
+
return new AdminDocumentSnapshot(row.id, row.data, new DocumentRef(this.collectionName, row.id, this.parentPath));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async getDocs(client = pool) {
|
|
374
|
+
const { whereSql, params } = buildWhere(this._filters);
|
|
375
|
+
const orderSql = buildOrder(this._order);
|
|
376
|
+
const limitSql = this._limit ? `LIMIT ${this._limit}` : "";
|
|
377
|
+
const offsetSql = this._offset ? `OFFSET ${this._offset}` : "";
|
|
378
|
+
|
|
379
|
+
const sql = `
|
|
380
|
+
SELECT id, data, created_at, updated_at
|
|
381
|
+
FROM "${this.collectionName}"
|
|
382
|
+
${whereSql} ${orderSql} ${limitSql} ${offsetSql}`;
|
|
383
|
+
const result = await runQuery(client, sql, params);
|
|
384
|
+
return result.rows.map(r => new AdminDocumentSnapshot(r.id, r.data, new DocumentRef(this.collectionName, r.id, this.parentPath)));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async get(client = pool) {
|
|
388
|
+
const docs = await this.getDocs(client);
|
|
389
|
+
return new AdminQuerySnapshot(docs);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* ------------------------------------------------------------------ */
|
|
394
|
+
/* Transaction Support (Firestore-style) */
|
|
395
|
+
/* ------------------------------------------------------------------ */
|
|
396
|
+
async function runTransaction(fn, client = pool) {
|
|
397
|
+
await client.query("BEGIN");
|
|
398
|
+
const ops = [];
|
|
399
|
+
|
|
400
|
+
const tx = {
|
|
401
|
+
get: (ref) => ref.get(client),
|
|
402
|
+
set: (ref, data, opts = {}) => ops.push(() => ref.set(data, opts, client)),
|
|
403
|
+
update: (ref, data) => ops.push(() => ref.update(data, client)),
|
|
404
|
+
delete: (ref) => ops.push(() => ref.delete(client)),
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
await fn(tx);
|
|
409
|
+
for (const op of ops) await op();
|
|
410
|
+
await client.query("COMMIT");
|
|
411
|
+
} catch (err) {
|
|
412
|
+
await client.query("ROLLBACK");
|
|
413
|
+
throw err;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isDocumentRef(value) {
|
|
418
|
+
if (!value || typeof value !== "object") return false;
|
|
419
|
+
if (value.id && value.path && value.collectionName) return true;
|
|
420
|
+
if (value._type === 'ref' && value.path) return true;
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function resolveReference(ref) {
|
|
425
|
+
if (ref.collectionName && ref.id) return `${ref.collectionName}/${ref.id}`;
|
|
426
|
+
if (ref._type === 'ref' && ref.path) return ref.path;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* ------------------------------------------------------------------ */
|
|
430
|
+
/* Postbase Admin Client (Firestore-like for Postgres JSONB) */
|
|
431
|
+
/* ------------------------------------------------------------------ */
|
|
432
|
+
|
|
433
|
+
function serializeForWrite(value) {
|
|
434
|
+
if (isDocumentRef(value)) {
|
|
435
|
+
console.log('isDocumentRef(value) === true');
|
|
436
|
+
return { _type: 'ref', path: resolveReference(value) };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (value instanceof Timestamp) {
|
|
440
|
+
console.log('value instanceof Timestamp === true');
|
|
441
|
+
return value.toString();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (Array.isArray(value)) {
|
|
445
|
+
console.log('Array.isArray(value) === true');
|
|
446
|
+
return value.map(serializeForWrite);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (typeof value === "object" && value !== null) {
|
|
450
|
+
console.log('(typeof value === "object" && value !== null) === true');
|
|
451
|
+
const out = {};
|
|
452
|
+
for (const [k, v] of Object.entries(value)) {
|
|
453
|
+
out[k] = serializeForWrite(v);
|
|
454
|
+
console.log('serializeForWrite', k, out[k]);
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return value;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function adminDeserialize(value) {
|
|
463
|
+
if (Array.isArray(value)) return value.map(adminDeserialize);
|
|
464
|
+
|
|
465
|
+
if (value && typeof value === "object") {
|
|
466
|
+
// detect timestamp
|
|
467
|
+
if (typeof value === "string" && isIsoDateString(value)) {
|
|
468
|
+
return Timestamp.fromPostgres(value);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// detect reference object
|
|
472
|
+
if (value._type === "ref" && value.path) {
|
|
473
|
+
const parts = value.path.split('/');
|
|
474
|
+
let ref = self.collection(parts[0]).doc(parts[1]);
|
|
475
|
+
for (let i = 2; i < parts.length; i += 2) {
|
|
476
|
+
ref = ref.collection(parts[i]).doc(parts[i + 1]);
|
|
477
|
+
}
|
|
478
|
+
return ref;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const out = {};
|
|
482
|
+
for (const [k, v] of Object.entries(value)) {
|
|
483
|
+
out[k] = adminDeserialize(v);
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ISO top-level primitive
|
|
489
|
+
if (typeof value === "string" && isIsoDateString(value)) {
|
|
490
|
+
return Timestamp.fromPostgres(value);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return value;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function isIsoDateString(v) {
|
|
497
|
+
return typeof v === "string" &&
|
|
498
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(v);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* ------------------------------------------------------------------ */
|
|
502
|
+
/* Public API */
|
|
503
|
+
/* ------------------------------------------------------------------ */
|
|
504
|
+
self = {
|
|
505
|
+
DocumentRef,
|
|
506
|
+
collection(name) {
|
|
507
|
+
return new CollectionRef(name);
|
|
508
|
+
},
|
|
509
|
+
runTransaction,
|
|
510
|
+
FieldValue,
|
|
511
|
+
Timestamp,
|
|
512
|
+
FieldPath,
|
|
513
|
+
documentId,
|
|
514
|
+
|
|
515
|
+
// need that for websockets
|
|
516
|
+
buildWhere,
|
|
517
|
+
buildOrder,
|
|
518
|
+
};
|
|
519
|
+
return self;
|
|
520
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Backward compatability with admin firebase
|
|
2
|
+
|
|
3
|
+
import { makePostbaseAdminClient } from '../adminClient.js';
|
|
4
|
+
|
|
5
|
+
export function createAdminClient(
|
|
6
|
+
{
|
|
7
|
+
authClient = null,
|
|
8
|
+
pool = null,
|
|
9
|
+
} = {}) {
|
|
10
|
+
return {
|
|
11
|
+
auth: () => {
|
|
12
|
+
if (!authClient) throw new Error("authClient is missing. please check postbase/backend/auth.js");
|
|
13
|
+
return {
|
|
14
|
+
getUser: async (userId) => {
|
|
15
|
+
const { data: user, error } = await authClient.admin.getUser({ userId });
|
|
16
|
+
const userObj = {
|
|
17
|
+
...user,
|
|
18
|
+
displayName: user.name,
|
|
19
|
+
};
|
|
20
|
+
return userObj;
|
|
21
|
+
},
|
|
22
|
+
// TODO
|
|
23
|
+
// deleteUser: async (uidToDelete) => {
|
|
24
|
+
// const { data: user, error } = await authClient.admin.removeUser({
|
|
25
|
+
// userId: uidToDelete,
|
|
26
|
+
// });
|
|
27
|
+
// if (error) {
|
|
28
|
+
// throw error;
|
|
29
|
+
// }
|
|
30
|
+
// const userObj = {
|
|
31
|
+
// ...user,
|
|
32
|
+
// displayName: user.name,
|
|
33
|
+
// };
|
|
34
|
+
// return userObj;
|
|
35
|
+
// },
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
firestore: () => {
|
|
39
|
+
if (!pool) throw new Error("pool is missing. please check postbase/backend/app.js or use createPool from backend/lib/postbase/db.js");
|
|
40
|
+
const db = makePostbaseAdminClient({ pool });
|
|
41
|
+
return db;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// db.js
|
|
2
|
+
import { Pool } from 'pg';
|
|
3
|
+
|
|
4
|
+
export function createPool(opts = {}) {
|
|
5
|
+
const pool = new Pool(opts);
|
|
6
|
+
return pool;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function runQuery(pool, text, params = []) {
|
|
10
|
+
const client = await pool.connect();
|
|
11
|
+
try {
|
|
12
|
+
const res = await client.query(text, params);
|
|
13
|
+
return res;
|
|
14
|
+
} finally {
|
|
15
|
+
client.release();
|
|
16
|
+
}
|
|
17
|
+
}
|