@rebasepro/server-postgresql 0.1.0 → 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.
Files changed (75) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/util/entities.d.ts +2 -2
  3. package/dist/common/src/util/relations.d.ts +1 -1
  4. package/dist/index.es.js +1250 -1665
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1196 -1611
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  10. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  11. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
  12. package/dist/server-postgresql/src/auth/services.d.ts +37 -15
  13. package/dist/server-postgresql/src/index.d.ts +1 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
  15. package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
  16. package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
  17. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
  18. package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
  19. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  20. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  21. package/dist/types/src/controllers/auth.d.ts +9 -8
  22. package/dist/types/src/controllers/client.d.ts +3 -0
  23. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  24. package/dist/types/src/types/collections.d.ts +67 -2
  25. package/dist/types/src/types/database_adapter.d.ts +94 -0
  26. package/dist/types/src/types/entity_actions.d.ts +7 -1
  27. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  28. package/dist/types/src/types/entity_views.d.ts +36 -1
  29. package/dist/types/src/types/index.d.ts +2 -0
  30. package/dist/types/src/types/plugins.d.ts +1 -1
  31. package/dist/types/src/types/properties.d.ts +24 -5
  32. package/dist/types/src/types/property_config.d.ts +6 -2
  33. package/dist/types/src/types/relations.d.ts +1 -1
  34. package/dist/types/src/types/translations.d.ts +8 -0
  35. package/dist/types/src/users/user.d.ts +5 -0
  36. package/package.json +21 -15
  37. package/src/PostgresAdapter.ts +59 -0
  38. package/src/PostgresBackendDriver.ts +57 -8
  39. package/src/PostgresBootstrapper.ts +35 -15
  40. package/src/auth/ensure-tables.ts +82 -189
  41. package/src/auth/services.ts +421 -170
  42. package/src/cli.ts +44 -13
  43. package/src/data-transformer.ts +78 -8
  44. package/src/history/HistoryService.ts +25 -2
  45. package/src/index.ts +1 -0
  46. package/src/schema/auth-schema.ts +130 -98
  47. package/src/schema/default-collections.ts +68 -0
  48. package/src/schema/doctor-cli.ts +5 -1
  49. package/src/schema/doctor.ts +85 -8
  50. package/src/schema/generate-drizzle-schema-logic.ts +74 -27
  51. package/src/schema/generate-drizzle-schema.ts +13 -3
  52. package/src/schema/introspect-db-inference.ts +5 -5
  53. package/src/schema/introspect-db-logic.ts +9 -2
  54. package/src/schema/introspect-db.ts +14 -3
  55. package/src/services/EntityFetchService.ts +5 -5
  56. package/src/services/RelationService.ts +2 -2
  57. package/src/services/entity-helpers.ts +1 -1
  58. package/src/services/realtimeService.ts +145 -136
  59. package/src/utils/drizzle-conditions.ts +16 -2
  60. package/src/websocket.ts +113 -37
  61. package/test/auth-services.test.ts +163 -74
  62. package/test/data-transformer-hardening.test.ts +57 -0
  63. package/test/data-transformer.test.ts +43 -0
  64. package/test/generate-drizzle-schema.test.ts +7 -5
  65. package/test/introspect-db-utils.test.ts +4 -1
  66. package/test/postgresDataDriver.test.ts +17 -0
  67. package/test/realtimeService.test.ts +7 -7
  68. package/test/websocket.test.ts +139 -0
  69. package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
  70. package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
  71. package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
  72. package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
  73. package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
  74. package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
  75. package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
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 {
@@ -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("..", "shared", "collections");
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("..", "shared", "collections");
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 {
@@ -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 }) => ({
@@ -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
+ };