@rebasepro/server-postgresql 0.1.2 → 0.2.1
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/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1160 -612
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1158 -610
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +21 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +57 -8
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +44 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +68 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +85 -8
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +17 -0
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import arg from "arg";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import execa from "execa";
|
|
3
|
+
import { execa } from "execa";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import { execSync } from "child_process";
|
|
@@ -11,14 +11,17 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
function resolveLocalBin(binName: string): string | null {
|
|
12
12
|
let cwd = process.cwd();
|
|
13
13
|
// Try to find node_modules/.bin upwards
|
|
14
|
-
while (
|
|
14
|
+
while (true) {
|
|
15
15
|
const candidate = path.join(cwd, "node_modules", ".bin", binName);
|
|
16
16
|
if (fs.existsSync(candidate)) return candidate;
|
|
17
|
-
|
|
17
|
+
const parent = path.dirname(cwd);
|
|
18
|
+
if (parent === cwd) break;
|
|
19
|
+
cwd = parent;
|
|
18
20
|
}
|
|
19
|
-
// Fall back to globally installed binary via which
|
|
21
|
+
// Fall back to globally installed binary via which/where
|
|
20
22
|
try {
|
|
21
|
-
const
|
|
23
|
+
const cmd = process.platform === "win32" ? `where ${binName}` : `which ${binName}`;
|
|
24
|
+
const globalPath = execSync(cmd, { encoding: "utf-8" }).trim().split("\n")[0].trim();
|
|
22
25
|
if (globalPath && fs.existsSync(globalPath)) return globalPath;
|
|
23
26
|
} catch {
|
|
24
27
|
// not found globally
|
|
@@ -89,6 +92,13 @@ async function dbCommand(subcommand: string, rawArgs: string[]): Promise<void> {
|
|
|
89
92
|
await runDrizzleKit("push", rawArgs);
|
|
90
93
|
} else if (subcommand === "migrate") {
|
|
91
94
|
await runDrizzleKit("migrate", rawArgs);
|
|
95
|
+
} else if (subcommand === "studio") {
|
|
96
|
+
const schemaPath = path.join(process.cwd(), "src", "schema.generated.ts");
|
|
97
|
+
if (!fs.existsSync(schemaPath)) {
|
|
98
|
+
console.log(chalk.yellow(" ⚠ schema.generated.ts not found. Generating schema first..."));
|
|
99
|
+
await schemaCommand("generate", rawArgs);
|
|
100
|
+
}
|
|
101
|
+
await runDrizzleKit("studio", rawArgs);
|
|
92
102
|
} else {
|
|
93
103
|
await runDrizzleKit(subcommand, rawArgs);
|
|
94
104
|
}
|
|
@@ -312,7 +322,12 @@ async function fixMigrationStatementOrder(): Promise<void> {
|
|
|
312
322
|
if (sqlFiles.length === 0) return;
|
|
313
323
|
|
|
314
324
|
const latestFile = path.join(drizzleDir, sqlFiles[0].name);
|
|
315
|
-
|
|
325
|
+
let content = fs.readFileSync(latestFile, "utf-8");
|
|
326
|
+
const originalContent = content;
|
|
327
|
+
|
|
328
|
+
// Replace CREATE SCHEMA with CREATE SCHEMA IF NOT EXISTS to prevent failures
|
|
329
|
+
content = content.replace(/CREATE SCHEMA "([^"]+)";/g, 'CREATE SCHEMA IF NOT EXISTS "$1";');
|
|
330
|
+
|
|
316
331
|
const DELIMITER = "--> statement-breakpoint";
|
|
317
332
|
const parts = content.split(DELIMITER);
|
|
318
333
|
|
|
@@ -354,7 +369,12 @@ async function fixMigrationStatementOrder(): Promise<void> {
|
|
|
354
369
|
if (needsReorder) break;
|
|
355
370
|
}
|
|
356
371
|
|
|
357
|
-
if (!needsReorder)
|
|
372
|
+
if (!needsReorder) {
|
|
373
|
+
if (content !== originalContent) {
|
|
374
|
+
fs.writeFileSync(latestFile, content, "utf-8");
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
358
378
|
|
|
359
379
|
// Reorder: move DROP POLICY statements for affected tables before their ALTER TABLE
|
|
360
380
|
// Strategy: stable sort — DROP POLICY on table X gets priority over ALTER on table X
|
|
@@ -378,7 +398,7 @@ idx }));
|
|
|
378
398
|
const reordered = stmtEntries.map(e => e.stmt).join(DELIMITER);
|
|
379
399
|
fs.writeFileSync(latestFile, reordered, "utf-8");
|
|
380
400
|
|
|
381
|
-
console.log(chalk.yellow(`
|
|
401
|
+
console.log(chalk.yellow(` \u26A0 Reordered migration statements in ${sqlFiles[0].name} (DROP POLICY before ALTER COLUMN)`));
|
|
382
402
|
}
|
|
383
403
|
|
|
384
404
|
async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void> {
|
|
@@ -403,7 +423,11 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
403
423
|
if (fs.existsSync(p)) {
|
|
404
424
|
const parsed = dotenv.config({ path: p });
|
|
405
425
|
if (parsed.parsed) {
|
|
406
|
-
Object.
|
|
426
|
+
for (const [key, val] of Object.entries(parsed.parsed)) {
|
|
427
|
+
if (env[key] === undefined) {
|
|
428
|
+
env[key] = val;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
407
431
|
break;
|
|
408
432
|
}
|
|
409
433
|
}
|
|
@@ -419,6 +443,9 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
|
|
|
419
443
|
const drizzleKitArgs = [action];
|
|
420
444
|
if (action === "push") {
|
|
421
445
|
drizzleKitArgs.push("--strict", "--verbose");
|
|
446
|
+
if (_rawArgs.includes("--force")) {
|
|
447
|
+
drizzleKitArgs.push("--force");
|
|
448
|
+
}
|
|
422
449
|
}
|
|
423
450
|
|
|
424
451
|
try {
|
|
@@ -512,7 +539,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
|
|
|
512
539
|
process.exit(1);
|
|
513
540
|
}
|
|
514
541
|
|
|
515
|
-
const collectionsPath = argsList["--collections"] || path.join("..", "
|
|
542
|
+
const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
|
|
516
543
|
const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
|
|
517
544
|
const watch = argsList["--watch"] || false;
|
|
518
545
|
|
|
@@ -604,8 +631,10 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
|
604
631
|
{
|
|
605
632
|
"--collections": String,
|
|
606
633
|
"--schema": String,
|
|
634
|
+
"--sdk": String,
|
|
607
635
|
"-c": "--collections",
|
|
608
|
-
"-s": "--schema"
|
|
636
|
+
"-s": "--schema",
|
|
637
|
+
"-k": "--sdk"
|
|
609
638
|
},
|
|
610
639
|
{
|
|
611
640
|
argv: rawArgs.slice(1), // skip "doctor"
|
|
@@ -625,14 +654,16 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
|
|
|
625
654
|
process.exit(1);
|
|
626
655
|
}
|
|
627
656
|
|
|
628
|
-
const collectionsPath = parsedArgs["--collections"] || path.join("..", "
|
|
657
|
+
const collectionsPath = parsedArgs["--collections"] || path.join("..", "config", "collections");
|
|
629
658
|
const schemaPath = parsedArgs["--schema"] || path.join("src", "schema.generated.ts");
|
|
659
|
+
const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
|
|
630
660
|
|
|
631
661
|
const cmdParts = [
|
|
632
662
|
tsxBin,
|
|
633
663
|
doctorScript,
|
|
634
664
|
`--collections=${collectionsPath}`,
|
|
635
|
-
`--schema=${schemaPath}
|
|
665
|
+
`--schema=${schemaPath}`,
|
|
666
|
+
`--sdk=${sdkPath}`
|
|
636
667
|
];
|
|
637
668
|
|
|
638
669
|
try {
|
package/src/data-transformer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq, SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn } from "drizzle-orm/pg-core";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import { CollectionWithRelations, EntityCollection, Properties, Property, Relation, RelationProperty } from "@rebasepro/types";
|
|
4
|
+
import { CollectionWithRelations, EntityCollection, Properties, Property, Relation, RelationProperty, Vector, BinaryProperty } from "@rebasepro/types";
|
|
5
5
|
import { getTableName, resolveCollectionRelations, findRelation, createRelationRef, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "@rebasepro/common";
|
|
6
6
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
7
7
|
import { DrizzleConditionBuilder } from "./utils/drizzle-conditions";
|
|
@@ -127,6 +127,8 @@ export function serializeDataToServer<M extends Record<string, unknown>>(
|
|
|
127
127
|
continue;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
|
|
131
|
+
|
|
130
132
|
// Handle relation properties specially
|
|
131
133
|
if (property.type === "relation" && collection) {
|
|
132
134
|
const relation = findRelation(resolvedRelations, key);
|
|
@@ -257,6 +259,32 @@ export function serializePropertyToServer(value: unknown, property: Property): u
|
|
|
257
259
|
return result;
|
|
258
260
|
}
|
|
259
261
|
return value;
|
|
262
|
+
case "vector": {
|
|
263
|
+
if (value instanceof Vector) {
|
|
264
|
+
return value.value;
|
|
265
|
+
}
|
|
266
|
+
if (value && typeof value === "object" && "value" in value && Array.isArray((value as any).value)) {
|
|
267
|
+
return (value as any).value.map(Number);
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
return value.map(Number);
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case "binary":
|
|
276
|
+
if (typeof value === "string") {
|
|
277
|
+
if (value.startsWith("data:application/octet-stream;base64,")) {
|
|
278
|
+
const base64Data = value.split(",")[1];
|
|
279
|
+
if (base64Data) {
|
|
280
|
+
return Buffer.from(base64Data, "base64");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (Buffer.isBuffer(value)) {
|
|
285
|
+
return value;
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
260
288
|
|
|
261
289
|
case "string":
|
|
262
290
|
if (typeof value === "string") {
|
|
@@ -405,7 +433,7 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
|
|
|
405
433
|
// Add where condition for the current entity
|
|
406
434
|
if (pks.length === 1) {
|
|
407
435
|
const sourceIdField = sourceTable[pks[0].fieldName as keyof typeof sourceTable] as AnyPgColumn;
|
|
408
|
-
query = query.where(eq(sourceIdField, currentEntityId)) as
|
|
436
|
+
query = query.where(eq(sourceIdField, currentEntityId)) as typeof query;
|
|
409
437
|
} else {
|
|
410
438
|
// For composite keys, we would need to map the split parts. For now log a warning.
|
|
411
439
|
console.warn(`Join path resolution for composite primary keys is not yet fully supported: ${collection.slug}`);
|
|
@@ -466,6 +494,22 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
466
494
|
}
|
|
467
495
|
|
|
468
496
|
switch (property.type) {
|
|
497
|
+
case "binary": {
|
|
498
|
+
let buf: Buffer | null = null;
|
|
499
|
+
if (Buffer.isBuffer(value)) {
|
|
500
|
+
buf = value;
|
|
501
|
+
} else if (typeof value === "object" && value !== null) {
|
|
502
|
+
const rawVal = value as Record<string, unknown>;
|
|
503
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
504
|
+
buf = Buffer.from(rawVal.data as number[]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (buf) {
|
|
508
|
+
return `data:application/octet-stream;base64,${buf.toString("base64")}`;
|
|
509
|
+
}
|
|
510
|
+
return value;
|
|
511
|
+
}
|
|
512
|
+
|
|
469
513
|
case "string": {
|
|
470
514
|
if (typeof value === "string") return value;
|
|
471
515
|
|
|
@@ -476,9 +520,12 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
476
520
|
if (Buffer.isBuffer(value)) {
|
|
477
521
|
isBuffer = true;
|
|
478
522
|
buf = value;
|
|
479
|
-
} else if (typeof value === "object" && value !== null
|
|
480
|
-
|
|
481
|
-
|
|
523
|
+
} else if (typeof value === "object" && value !== null) {
|
|
524
|
+
const rawVal = value as Record<string, unknown>;
|
|
525
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
526
|
+
isBuffer = true;
|
|
527
|
+
buf = Buffer.from(rawVal.data as number[]);
|
|
528
|
+
}
|
|
482
529
|
}
|
|
483
530
|
|
|
484
531
|
if (isBuffer && buf) {
|
|
@@ -577,6 +624,26 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
577
624
|
}
|
|
578
625
|
return value;
|
|
579
626
|
|
|
627
|
+
case "vector": {
|
|
628
|
+
let nums: number[] = [];
|
|
629
|
+
if (typeof value === "string") {
|
|
630
|
+
nums = value.slice(1, -1).split(",").map(Number);
|
|
631
|
+
} else if (Array.isArray(value)) {
|
|
632
|
+
nums = value.map(Number);
|
|
633
|
+
} else if (value instanceof Vector) {
|
|
634
|
+
nums = value.value;
|
|
635
|
+
} else if (typeof value === "object" && value !== null && "value" in value) {
|
|
636
|
+
const valObj = value as { value: unknown };
|
|
637
|
+
if (Array.isArray(valObj.value)) {
|
|
638
|
+
nums = valObj.value.map(Number);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
__type: "Vector",
|
|
643
|
+
value: nums
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
580
647
|
case "date": {
|
|
581
648
|
let date: Date | undefined;
|
|
582
649
|
if (value instanceof Date) {
|
|
@@ -604,9 +671,12 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
604
671
|
if (Buffer.isBuffer(value)) {
|
|
605
672
|
isBuffer = true;
|
|
606
673
|
buf = value;
|
|
607
|
-
} else if (typeof value === "object" && value !== null
|
|
608
|
-
|
|
609
|
-
|
|
674
|
+
} else if (typeof value === "object" && value !== null) {
|
|
675
|
+
const rawVal = value as Record<string, unknown>;
|
|
676
|
+
if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
|
|
677
|
+
isBuffer = true;
|
|
678
|
+
buf = Buffer.from(rawVal.data as number[]);
|
|
679
|
+
}
|
|
610
680
|
}
|
|
611
681
|
|
|
612
682
|
if (isBuffer && buf) {
|
|
@@ -209,6 +209,29 @@ export class HistoryService {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Deep equality without JSON.stringify.
|
|
214
|
+
* Handles primitives, arrays, Dates, and plain objects recursively.
|
|
215
|
+
*/
|
|
216
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
217
|
+
if (a === b) return true;
|
|
218
|
+
if (a == null || b == null) return false;
|
|
219
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
220
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
221
|
+
if (a.length !== b.length) return false;
|
|
222
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
223
|
+
}
|
|
224
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
225
|
+
const aObj = a as Record<string, unknown>;
|
|
226
|
+
const bObj = b as Record<string, unknown>;
|
|
227
|
+
const aKeys = Object.keys(aObj);
|
|
228
|
+
const bKeys = Object.keys(bObj);
|
|
229
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
230
|
+
return aKeys.every(k => deepEqual(aObj[k], bObj[k]));
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
212
235
|
/**
|
|
213
236
|
* Shallow comparison to find top-level keys that changed between two objects.
|
|
214
237
|
*/
|
|
@@ -230,12 +253,12 @@ export function findChangedFields(
|
|
|
230
253
|
if (key.startsWith("__")) continue;
|
|
231
254
|
|
|
232
255
|
if (oldVal !== newVal) {
|
|
233
|
-
// For objects/arrays, use
|
|
256
|
+
// For objects/arrays, use structural comparison
|
|
234
257
|
if (
|
|
235
258
|
typeof oldVal === "object" && oldVal !== null &&
|
|
236
259
|
typeof newVal === "object" && newVal !== null
|
|
237
260
|
) {
|
|
238
|
-
if (
|
|
261
|
+
if (!deepEqual(oldVal, newVal)) {
|
|
239
262
|
changed.push(key);
|
|
240
263
|
}
|
|
241
264
|
} else {
|
package/src/index.ts
CHANGED
|
@@ -1,116 +1,148 @@
|
|
|
1
|
-
import { pgSchema, varchar, uuid, timestamp, boolean, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
|
|
1
|
+
import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
|
|
2
2
|
import { relations } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* Keeps the user's `public` schema clean.
|
|
5
|
+
* Factory function to dynamically create the auth tables bound to the specified schema names.
|
|
7
6
|
*/
|
|
8
|
-
export
|
|
7
|
+
export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchemaName: string = "rebase") {
|
|
8
|
+
const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
|
|
9
|
+
const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*/
|
|
13
|
-
export const users = rebaseSchema.table("users", {
|
|
14
|
-
id: uuid("id").defaultRandom().primaryKey(),
|
|
15
|
-
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
16
|
-
passwordHash: varchar("password_hash", { length: 255 }), // NULL for OAuth-only users
|
|
17
|
-
displayName: varchar("display_name", { length: 255 }),
|
|
18
|
-
photoUrl: varchar("photo_url", { length: 500 }),
|
|
19
|
-
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
20
|
-
emailVerificationToken: varchar("email_verification_token", { length: 255 }),
|
|
21
|
-
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
22
|
-
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
23
|
-
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
24
|
-
});
|
|
11
|
+
const rolesTableCreator: any = rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable;
|
|
12
|
+
const usersTableCreator: any = usersSchema ? usersSchema.table.bind(usersSchema) : pgTable;
|
|
25
13
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Users table - stores both email/password and OAuth users
|
|
16
|
+
*/
|
|
17
|
+
const users = usersTableCreator("users", {
|
|
18
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
19
|
+
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
20
|
+
passwordHash: varchar("password_hash", { length: 255 }), // NULL for OAuth-only users
|
|
21
|
+
displayName: varchar("display_name", { length: 255 }),
|
|
22
|
+
photoUrl: varchar("photo_url", { length: 500 }),
|
|
23
|
+
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
24
|
+
emailVerificationToken: varchar("email_verification_token", { length: 255 }),
|
|
25
|
+
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
26
|
+
metadata: jsonb("metadata").$type<Record<string, any>>().default({}).notNull(),
|
|
27
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
28
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Roles table - defines permission sets
|
|
33
|
+
*/
|
|
34
|
+
const roles = rolesTableCreator("roles", {
|
|
35
|
+
id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
|
|
36
|
+
name: varchar("name", { length: 100 }).notNull(),
|
|
37
|
+
isAdmin: boolean("is_admin").default(false).notNull(),
|
|
38
|
+
defaultPermissions: jsonb("default_permissions").$type<{
|
|
41
39
|
read?: boolean;
|
|
42
40
|
create?: boolean;
|
|
43
41
|
edit?: boolean;
|
|
44
42
|
delete?: boolean;
|
|
45
|
-
}>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
43
|
+
}>(),
|
|
44
|
+
collectionPermissions: jsonb("collection_permissions").$type<
|
|
45
|
+
Record<string, {
|
|
46
|
+
read?: boolean;
|
|
47
|
+
create?: boolean;
|
|
48
|
+
edit?: boolean;
|
|
49
|
+
delete?: boolean;
|
|
50
|
+
}>
|
|
51
|
+
>(),
|
|
52
|
+
config: jsonb("config").$type<{
|
|
53
|
+
createCollections?: boolean;
|
|
54
|
+
editCollections?: "own" | "all" | boolean;
|
|
55
|
+
deleteCollections?: "own" | "all" | boolean;
|
|
56
|
+
}>()
|
|
57
|
+
});
|
|
53
58
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}, (table) => ({
|
|
61
|
-
|
|
62
|
-
}));
|
|
59
|
+
/**
|
|
60
|
+
* User-Role junction table
|
|
61
|
+
*/
|
|
62
|
+
const userRoles = rolesTableCreator("user_roles", {
|
|
63
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
64
|
+
roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
|
|
65
|
+
}, (table: any) => ({
|
|
66
|
+
pk: primaryKey({ columns: [table.userId, table.roleId] })
|
|
67
|
+
}));
|
|
63
68
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}, (table) => ({
|
|
76
|
-
|
|
77
|
-
}));
|
|
69
|
+
/**
|
|
70
|
+
* Refresh tokens for long-lived sessions
|
|
71
|
+
*/
|
|
72
|
+
const refreshTokens = rolesTableCreator("refresh_tokens", {
|
|
73
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
74
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
75
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
76
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
77
|
+
userAgent: varchar("user_agent", { length: 500 }),
|
|
78
|
+
ipAddress: varchar("ip_address", { length: 45 }),
|
|
79
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
80
|
+
}, (table: any) => ({
|
|
81
|
+
uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
|
|
82
|
+
}));
|
|
78
83
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
84
|
+
/**
|
|
85
|
+
* Password reset tokens for forgot password flow
|
|
86
|
+
*/
|
|
87
|
+
const passwordResetTokens = rolesTableCreator("password_reset_tokens", {
|
|
88
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
89
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
90
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
91
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
92
|
+
usedAt: timestamp("used_at"),
|
|
93
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
94
|
+
});
|
|
90
95
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
96
|
+
/**
|
|
97
|
+
* App config - key/value store for custom settings
|
|
98
|
+
*/
|
|
99
|
+
const appConfig = rolesTableCreator("app_config", {
|
|
100
|
+
key: varchar("key", { length: 100 }).primaryKey(),
|
|
101
|
+
value: jsonb("value").notNull(),
|
|
102
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
103
|
+
});
|
|
99
104
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}, (table) => ({
|
|
112
|
-
|
|
113
|
-
}));
|
|
105
|
+
/**
|
|
106
|
+
* User identities - maps external OAuth profiles back to local users
|
|
107
|
+
*/
|
|
108
|
+
const userIdentities = rolesTableCreator("user_identities", {
|
|
109
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
110
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
111
|
+
provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
|
|
112
|
+
providerId: varchar("provider_id", { length: 255 }).notNull(),
|
|
113
|
+
profileData: jsonb("profile_data"),
|
|
114
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
115
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
116
|
+
}, (table: any) => ({
|
|
117
|
+
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
rolesSchema,
|
|
122
|
+
usersSchema,
|
|
123
|
+
users,
|
|
124
|
+
roles,
|
|
125
|
+
userRoles,
|
|
126
|
+
refreshTokens,
|
|
127
|
+
passwordResetTokens,
|
|
128
|
+
appConfig,
|
|
129
|
+
userIdentities
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Instantiate default schema and tables using the default "rebase" schema
|
|
134
|
+
const defaultAuthSchema = createAuthSchema("rebase", "rebase");
|
|
135
|
+
|
|
136
|
+
export const rebaseSchema = defaultAuthSchema.rolesSchema;
|
|
137
|
+
export const usersSchema = defaultAuthSchema.usersSchema;
|
|
138
|
+
|
|
139
|
+
export const users = defaultAuthSchema.users;
|
|
140
|
+
export const roles = defaultAuthSchema.roles;
|
|
141
|
+
export const userRoles = defaultAuthSchema.userRoles;
|
|
142
|
+
export const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
143
|
+
export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
144
|
+
export const appConfig = defaultAuthSchema.appConfig;
|
|
145
|
+
export const userIdentities = defaultAuthSchema.userIdentities;
|
|
114
146
|
|
|
115
147
|
// Relations
|
|
116
148
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PostgresCollection } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
export const defaultUsersCollection: PostgresCollection = {
|
|
4
|
+
name: "Users",
|
|
5
|
+
singularName: "User",
|
|
6
|
+
slug: "users",
|
|
7
|
+
table: "users",
|
|
8
|
+
icon: "Users",
|
|
9
|
+
group: "Settings",
|
|
10
|
+
properties: {
|
|
11
|
+
id: {
|
|
12
|
+
name: "ID",
|
|
13
|
+
type: "string",
|
|
14
|
+
isId: "uuid"
|
|
15
|
+
},
|
|
16
|
+
email: {
|
|
17
|
+
name: "Email",
|
|
18
|
+
type: "string",
|
|
19
|
+
validation: { required: true, unique: true }
|
|
20
|
+
},
|
|
21
|
+
password_hash: {
|
|
22
|
+
name: "Password Hash",
|
|
23
|
+
type: "string",
|
|
24
|
+
ui: { hideFromCollection: true }
|
|
25
|
+
},
|
|
26
|
+
display_name: {
|
|
27
|
+
name: "Display Name",
|
|
28
|
+
type: "string"
|
|
29
|
+
},
|
|
30
|
+
photo_url: {
|
|
31
|
+
name: "Photo URL",
|
|
32
|
+
type: "string"
|
|
33
|
+
},
|
|
34
|
+
email_verified: {
|
|
35
|
+
name: "Email Verified",
|
|
36
|
+
type: "boolean",
|
|
37
|
+
defaultValue: false
|
|
38
|
+
},
|
|
39
|
+
email_verification_token: {
|
|
40
|
+
name: "Email Verification Token",
|
|
41
|
+
type: "string",
|
|
42
|
+
ui: { hideFromCollection: true }
|
|
43
|
+
},
|
|
44
|
+
email_verification_sent_at: {
|
|
45
|
+
name: "Email Verification Sent At",
|
|
46
|
+
type: "date",
|
|
47
|
+
ui: { hideFromCollection: true }
|
|
48
|
+
},
|
|
49
|
+
metadata: {
|
|
50
|
+
name: "Metadata",
|
|
51
|
+
type: "map",
|
|
52
|
+
defaultValue: {},
|
|
53
|
+
ui: { hideFromCollection: true }
|
|
54
|
+
},
|
|
55
|
+
created_at: {
|
|
56
|
+
name: "Created At",
|
|
57
|
+
type: "date",
|
|
58
|
+
autoValue: "on_create",
|
|
59
|
+
ui: { readOnly: true, hideFromCollection: true }
|
|
60
|
+
},
|
|
61
|
+
updated_at: {
|
|
62
|
+
name: "Updated At",
|
|
63
|
+
type: "date",
|
|
64
|
+
autoValue: "on_update",
|
|
65
|
+
ui: { readOnly: true, hideFromCollection: true }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|