@rebasepro/server-postgresql 0.1.2 → 0.2.3

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 (71) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/data/query_builder.d.ts +51 -0
  3. package/dist/common/src/index.d.ts +1 -0
  4. package/dist/common/src/util/entities.d.ts +2 -2
  5. package/dist/common/src/util/relations.d.ts +1 -1
  6. package/dist/index.es.js +1435 -738
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +1433 -736
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  11. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  12. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  13. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
  14. package/dist/server-postgresql/src/auth/services.d.ts +37 -15
  15. package/dist/server-postgresql/src/index.d.ts +1 -0
  16. package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
  17. package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
  18. package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
  19. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
  20. package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
  21. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  22. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  23. package/dist/types/src/controllers/auth.d.ts +9 -8
  24. package/dist/types/src/controllers/client.d.ts +3 -0
  25. package/dist/types/src/controllers/data.d.ts +21 -0
  26. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  27. package/dist/types/src/types/collections.d.ts +67 -2
  28. package/dist/types/src/types/database_adapter.d.ts +94 -0
  29. package/dist/types/src/types/entity_actions.d.ts +7 -1
  30. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +36 -1
  32. package/dist/types/src/types/index.d.ts +2 -0
  33. package/dist/types/src/types/plugins.d.ts +1 -1
  34. package/dist/types/src/types/properties.d.ts +24 -5
  35. package/dist/types/src/types/property_config.d.ts +6 -2
  36. package/dist/types/src/types/relations.d.ts +1 -1
  37. package/dist/types/src/types/translations.d.ts +8 -0
  38. package/dist/types/src/users/user.d.ts +5 -0
  39. package/package.json +22 -15
  40. package/src/PostgresAdapter.ts +59 -0
  41. package/src/PostgresBackendDriver.ts +66 -13
  42. package/src/PostgresBootstrapper.ts +35 -15
  43. package/src/auth/ensure-tables.ts +82 -189
  44. package/src/auth/services.ts +421 -170
  45. package/src/cli.ts +49 -13
  46. package/src/data-transformer.ts +78 -8
  47. package/src/history/HistoryService.ts +25 -2
  48. package/src/index.ts +1 -0
  49. package/src/schema/auth-schema.ts +130 -98
  50. package/src/schema/default-collections.ts +69 -0
  51. package/src/schema/doctor-cli.ts +5 -1
  52. package/src/schema/doctor.ts +166 -48
  53. package/src/schema/generate-drizzle-schema-logic.ts +74 -27
  54. package/src/schema/generate-drizzle-schema.ts +13 -3
  55. package/src/schema/introspect-db-inference.ts +5 -5
  56. package/src/schema/introspect-db-logic.ts +9 -2
  57. package/src/schema/introspect-db.ts +14 -3
  58. package/src/services/EntityFetchService.ts +5 -5
  59. package/src/services/RelationService.ts +2 -2
  60. package/src/services/entity-helpers.ts +1 -1
  61. package/src/services/realtimeService.ts +145 -136
  62. package/src/utils/drizzle-conditions.ts +16 -2
  63. package/src/websocket.ts +113 -37
  64. package/test/auth-services.test.ts +163 -74
  65. package/test/data-transformer-hardening.test.ts +57 -0
  66. package/test/data-transformer.test.ts +43 -0
  67. package/test/generate-drizzle-schema.test.ts +7 -5
  68. package/test/introspect-db-utils.test.ts +4 -1
  69. package/test/postgresDataDriver.test.ts +147 -1
  70. package/test/realtimeService.test.ts +7 -7
  71. 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 (cwd !== "/") {
14
+ while (true) {
15
15
  const candidate = path.join(cwd, "node_modules", ".bin", binName);
16
16
  if (fs.existsSync(candidate)) return candidate;
17
- cwd = path.dirname(cwd);
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 globalPath = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
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
- const content = fs.readFileSync(latestFile, "utf-8");
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) return;
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(` Reordered migration statements in ${sqlFiles[0].name} (DROP POLICY before ALTER COLUMN)`));
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.assign(env, parsed.parsed);
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 {
@@ -451,11 +478,16 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
451
478
  const errorOutput = stderr || stdout;
452
479
  if (errorOutput) {
453
480
  const lines = errorOutput.split("\n").filter((l: string) => l.trim());
481
+ let printedCount = 0;
454
482
  for (const line of lines) {
455
483
  if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
456
484
  console.error(chalk.red(` ${line.trim()}`));
485
+ printedCount++;
457
486
  }
458
487
  }
488
+ if (printedCount === 0) {
489
+ lines.slice(0, 10).forEach(line => console.error(chalk.red(` ${line.trim()}`)));
490
+ }
459
491
  }
460
492
  console.error("");
461
493
  process.exit(1);
@@ -512,7 +544,7 @@ async function schemaCommand(subcommand: string, rawArgs: string[]): Promise<voi
512
544
  process.exit(1);
513
545
  }
514
546
 
515
- const collectionsPath = argsList["--collections"] || path.join("..", "shared", "collections");
547
+ const collectionsPath = argsList["--collections"] || path.join("..", "config", "collections");
516
548
  const outputPath = argsList["--output"] || path.join("src", "schema.generated.ts");
517
549
  const watch = argsList["--watch"] || false;
518
550
 
@@ -604,8 +636,10 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
604
636
  {
605
637
  "--collections": String,
606
638
  "--schema": String,
639
+ "--sdk": String,
607
640
  "-c": "--collections",
608
- "-s": "--schema"
641
+ "-s": "--schema",
642
+ "-k": "--sdk"
609
643
  },
610
644
  {
611
645
  argv: rawArgs.slice(1), // skip "doctor"
@@ -625,14 +659,16 @@ async function doctorPluginCommand(rawArgs: string[]): Promise<void> {
625
659
  process.exit(1);
626
660
  }
627
661
 
628
- const collectionsPath = parsedArgs["--collections"] || path.join("..", "shared", "collections");
662
+ const collectionsPath = parsedArgs["--collections"] || path.join("..", "config", "collections");
629
663
  const schemaPath = parsedArgs["--schema"] || path.join("src", "schema.generated.ts");
664
+ const sdkPath = parsedArgs["--sdk"] || path.join("..", "generated", "sdk", "database.types.ts");
630
665
 
631
666
  const cmdParts = [
632
667
  tsxBin,
633
668
  doctorScript,
634
669
  `--collections=${collectionsPath}`,
635
- `--schema=${schemaPath}`
670
+ `--schema=${schemaPath}`,
671
+ `--sdk=${sdkPath}`
636
672
  ];
637
673
 
638
674
  try {
@@ -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 unknown as typeof query;
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 && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
480
- isBuffer = true;
481
- buf = Buffer.from((value as any).data);
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 && (value as any).type === "Buffer" && Array.isArray((value as any).data)) {
608
- isBuffer = true;
609
- buf = Buffer.from((value as any).data);
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 JSON comparison
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 (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
261
+ if (!deepEqual(oldVal, newVal)) {
239
262
  changed.push(key);
240
263
  }
241
264
  } else {
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./websocket";
11
11
  export * from "./collections/PostgresCollectionRegistry";
12
12
  export * from "./services/BranchService";
13
13
  export * from "./PostgresBootstrapper";
14
+ export * from "./PostgresAdapter";
@@ -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
- * Dedicated PostgreSQL schema for all Rebase internal tables.
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 const rebaseSchema = pgSchema("rebase");
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
- * Users table - stores both email/password and OAuth users
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
- * Roles table - defines permission sets
28
- */
29
- export const roles = rebaseSchema.table("roles", {
30
- id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
31
- name: varchar("name", { length: 100 }).notNull(),
32
- isAdmin: boolean("is_admin").default(false).notNull(),
33
- defaultPermissions: jsonb("default_permissions").$type<{
34
- read?: boolean;
35
- create?: boolean;
36
- edit?: boolean;
37
- delete?: boolean;
38
- }>(),
39
- collectionPermissions: jsonb("collection_permissions").$type<
40
- Record<string, {
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
- config: jsonb("config").$type<{
48
- createCollections?: boolean;
49
- editCollections?: "own" | "all" | boolean;
50
- deleteCollections?: "own" | "all" | boolean;
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
- * User-Role junction table
56
- */
57
- export const userRoles = rebaseSchema.table("user_roles", {
58
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
59
- roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
60
- }, (table) => ({
61
- pk: primaryKey({ columns: [table.userId, table.roleId] })
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
- * Refresh tokens for long-lived sessions
66
- */
67
- export const refreshTokens = rebaseSchema.table("refresh_tokens", {
68
- id: uuid("id").defaultRandom().primaryKey(),
69
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
70
- tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
71
- expiresAt: timestamp("expires_at").notNull(),
72
- userAgent: varchar("user_agent", { length: 500 }),
73
- ipAddress: varchar("ip_address", { length: 45 }),
74
- createdAt: timestamp("created_at").defaultNow().notNull()
75
- }, (table) => ({
76
- uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
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
- * Password reset tokens for forgot password flow
81
- */
82
- export const passwordResetTokens = rebaseSchema.table("password_reset_tokens", {
83
- id: uuid("id").defaultRandom().primaryKey(),
84
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
85
- tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
86
- expiresAt: timestamp("expires_at").notNull(),
87
- usedAt: timestamp("used_at"),
88
- createdAt: timestamp("created_at").defaultNow().notNull()
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
- * App config - key/value store for custom settings
93
- */
94
- export const appConfig = rebaseSchema.table("app_config", {
95
- key: varchar("key", { length: 100 }).primaryKey(),
96
- value: jsonb("value").notNull(),
97
- updatedAt: timestamp("updated_at").defaultNow().notNull()
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
- * User identities - maps external OAuth profiles back to local users
102
- */
103
- export const userIdentities = rebaseSchema.table("user_identities", {
104
- id: uuid("id").defaultRandom().primaryKey(),
105
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
106
- provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
107
- providerId: varchar("provider_id", { length: 255 }).notNull(),
108
- profileData: jsonb("profile_data"),
109
- createdAt: timestamp("created_at").defaultNow().notNull(),
110
- updatedAt: timestamp("updated_at").defaultNow().notNull()
111
- }, (table) => ({
112
- uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
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 }) => ({