@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e

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 (147) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +36 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +12 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +10635 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +10643 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +188 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +7 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
  43. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  45. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  46. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
  47. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  48. package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
  49. package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
  51. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  52. package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
  53. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  54. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  55. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  56. package/dist/types/src/controllers/auth.d.ts +117 -0
  57. package/dist/types/src/controllers/client.d.ts +58 -0
  58. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  59. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  60. package/dist/types/src/controllers/data.d.ts +141 -0
  61. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  62. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  63. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  64. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  65. package/dist/types/src/controllers/index.d.ts +17 -0
  66. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  67. package/dist/types/src/controllers/navigation.d.ts +213 -0
  68. package/dist/types/src/controllers/registry.d.ts +51 -0
  69. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  70. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  71. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  72. package/dist/types/src/controllers/storage.d.ts +173 -0
  73. package/dist/types/src/index.d.ts +4 -0
  74. package/dist/types/src/rebase_context.d.ts +101 -0
  75. package/dist/types/src/types/backend.d.ts +533 -0
  76. package/dist/types/src/types/builders.d.ts +14 -0
  77. package/dist/types/src/types/chips.d.ts +5 -0
  78. package/dist/types/src/types/collections.d.ts +812 -0
  79. package/dist/types/src/types/data_source.d.ts +64 -0
  80. package/dist/types/src/types/entities.d.ts +145 -0
  81. package/dist/types/src/types/entity_actions.d.ts +98 -0
  82. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  83. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  84. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  85. package/dist/types/src/types/entity_views.d.ts +61 -0
  86. package/dist/types/src/types/export_import.d.ts +21 -0
  87. package/dist/types/src/types/index.d.ts +22 -0
  88. package/dist/types/src/types/locales.d.ts +4 -0
  89. package/dist/types/src/types/modify_collections.d.ts +5 -0
  90. package/dist/types/src/types/plugins.d.ts +225 -0
  91. package/dist/types/src/types/properties.d.ts +1091 -0
  92. package/dist/types/src/types/property_config.d.ts +70 -0
  93. package/dist/types/src/types/relations.d.ts +336 -0
  94. package/dist/types/src/types/slots.d.ts +228 -0
  95. package/dist/types/src/types/translations.d.ts +826 -0
  96. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  97. package/dist/types/src/types/websockets.d.ts +78 -0
  98. package/dist/types/src/users/index.d.ts +2 -0
  99. package/dist/types/src/users/roles.d.ts +22 -0
  100. package/dist/types/src/users/user.d.ts +46 -0
  101. package/jest-all.log +3128 -0
  102. package/jest.log +49 -0
  103. package/package.json +93 -0
  104. package/src/PostgresBackendDriver.ts +1024 -0
  105. package/src/PostgresBootstrapper.ts +232 -0
  106. package/src/auth/ensure-tables.ts +309 -0
  107. package/src/auth/services.ts +740 -0
  108. package/src/cli.ts +347 -0
  109. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  110. package/src/connection.ts +62 -0
  111. package/src/data-transformer.ts +569 -0
  112. package/src/databasePoolManager.ts +84 -0
  113. package/src/history/HistoryService.ts +257 -0
  114. package/src/history/ensure-history-table.ts +45 -0
  115. package/src/index.ts +13 -0
  116. package/src/interfaces.ts +60 -0
  117. package/src/schema/auth-schema.ts +146 -0
  118. package/src/schema/generate-drizzle-schema-logic.ts +618 -0
  119. package/src/schema/generate-drizzle-schema.ts +151 -0
  120. package/src/services/BranchService.ts +237 -0
  121. package/src/services/EntityFetchService.ts +1447 -0
  122. package/src/services/EntityPersistService.ts +351 -0
  123. package/src/services/RelationService.ts +1012 -0
  124. package/src/services/entity-helpers.ts +121 -0
  125. package/src/services/entityService.ts +209 -0
  126. package/src/services/index.ts +13 -0
  127. package/src/services/realtimeService.ts +1005 -0
  128. package/src/utils/drizzle-conditions.ts +999 -0
  129. package/src/websocket.ts +487 -0
  130. package/test/auth-services.test.ts +569 -0
  131. package/test/branchService.test.ts +357 -0
  132. package/test/drizzle-conditions.test.ts +895 -0
  133. package/test/entityService.errors.test.ts +352 -0
  134. package/test/entityService.relations.test.ts +912 -0
  135. package/test/entityService.subcollection-search.test.ts +516 -0
  136. package/test/entityService.test.ts +977 -0
  137. package/test/generate-drizzle-schema.test.ts +795 -0
  138. package/test/historyService.test.ts +126 -0
  139. package/test/postgresDataDriver.test.ts +556 -0
  140. package/test/realtimeService.test.ts +276 -0
  141. package/test/relations.test.ts +662 -0
  142. package/test_drizzle_mock.js +3 -0
  143. package/test_find_changed.mjs +30 -0
  144. package/test_output.txt +3145 -0
  145. package/tsconfig.json +49 -0
  146. package/tsconfig.prod.json +20 -0
  147. package/vite.config.ts +82 -0
@@ -0,0 +1,257 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+
4
+ export interface HistoryEntry {
5
+ id: string;
6
+ table_name: string;
7
+ entity_id: string;
8
+ action: "create" | "update" | "delete";
9
+ changed_fields: string[] | null;
10
+ values: Record<string, unknown> | null;
11
+ previous_values: Record<string, unknown> | null;
12
+ updated_by: string | null;
13
+ updated_at: string;
14
+ }
15
+
16
+ export interface RecordHistoryParams {
17
+ tableName: string;
18
+ entityId: string;
19
+ action: "create" | "update" | "delete";
20
+ values?: Record<string, unknown> | null;
21
+ previousValues?: Record<string, unknown> | null;
22
+ updatedBy?: string | null;
23
+ }
24
+
25
+ export interface FetchHistoryOptions {
26
+ limit?: number;
27
+ offset?: number;
28
+ }
29
+
30
+ export interface HistoryRetentionConfig {
31
+ /** Max entries per entity. Oldest pruned first. Default 200. */
32
+ maxEntries: number;
33
+ /** Entries older than this many days are pruned. Default 90. */
34
+ ttlDays: number;
35
+ }
36
+
37
+ const DEFAULT_RETENTION: HistoryRetentionConfig = {
38
+ maxEntries: 200,
39
+ ttlDays: 90
40
+ };
41
+
42
+ /**
43
+ * Service for recording and querying entity change history.
44
+ * Stores snapshots in the `rebase.entity_history` table.
45
+ */
46
+ export class HistoryService {
47
+ public retention: HistoryRetentionConfig;
48
+
49
+ constructor(
50
+ private db: NodePgDatabase,
51
+ retention?: Partial<HistoryRetentionConfig>
52
+ ) {
53
+ this.retention = { ...DEFAULT_RETENTION, ...retention };
54
+ }
55
+
56
+ /**
57
+ * Record a history entry for an entity change.
58
+ * This is intentionally fire-and-forget safe — errors are logged but never
59
+ * bubble up to block the main save/delete operation.
60
+ *
61
+ * After inserting, kicks off a non-blocking pruning pass for this entity.
62
+ */
63
+ async recordHistory(params: RecordHistoryParams): Promise<void> {
64
+ const {
65
+ tableName,
66
+ entityId,
67
+ action,
68
+ values,
69
+ previousValues,
70
+ updatedBy
71
+ } = params;
72
+
73
+ const changedFields = previousValues && values
74
+ ? findChangedFields(previousValues, values)
75
+ : null;
76
+
77
+ try {
78
+ require('fs').appendFileSync(
79
+ '/Users/francesco/rebase/packages/backend/history_diff.log',
80
+ `[recordHistory: ${tableName}/${entityId} - ${action}]\n` +
81
+ `CHANGED FIELDS: ${JSON.stringify(changedFields)}\n` +
82
+ `PREVIOUS: ${JSON.stringify(previousValues, null, 2)}\n` +
83
+ `NEW: ${JSON.stringify(values, null, 2)}\n\n`
84
+ );
85
+ } catch (e) {
86
+ console.error("DEBUG FILE WRITE ERROR:", e);
87
+ }
88
+
89
+ // Skip recording if this is an update with zero actual changes
90
+ if (action === "update" && (!changedFields || changedFields.length === 0)) {
91
+ return;
92
+ }
93
+
94
+ try {
95
+ await this.db.execute(sql`
96
+ INSERT INTO rebase.entity_history
97
+ (table_name, entity_id, action, changed_fields, "values", previous_values, updated_by)
98
+ VALUES (
99
+ ${tableName},
100
+ ${String(entityId)},
101
+ ${action},
102
+ ${changedFields ? sql`ARRAY[${sql.join(changedFields.map(f => sql`${f}`), sql`, `)}]::text[]` : sql`NULL`},
103
+ ${values ? sql`${JSON.stringify(values)}::jsonb` : sql`NULL`},
104
+ ${previousValues ? sql`${JSON.stringify(previousValues)}::jsonb` : sql`NULL`},
105
+ ${updatedBy ?? null}
106
+ )
107
+ `);
108
+
109
+ // Non-blocking prune for this specific entity
110
+ this.pruneEntity(tableName, entityId).catch(err =>
111
+ console.error("History prune failed:", err)
112
+ );
113
+ } catch (error) {
114
+ console.error("Failed to record entity history:", error);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Fetch history entries for an entity, ordered by most recent first.
120
+ */
121
+ async fetchHistory(
122
+ tableName: string,
123
+ entityId: string,
124
+ options: FetchHistoryOptions = {}
125
+ ): Promise<{ data: HistoryEntry[]; total: number }> {
126
+ const limit = options.limit ?? 20;
127
+ const offset = options.offset ?? 0;
128
+
129
+ const [countResult, dataResult] = await Promise.all([
130
+ this.db.execute(sql`
131
+ SELECT COUNT(*) as count
132
+ FROM rebase.entity_history
133
+ WHERE table_name = ${tableName}
134
+ AND entity_id = ${String(entityId)}
135
+ `),
136
+ this.db.execute(sql`
137
+ SELECT id, table_name, entity_id, action, changed_fields,
138
+ "values", previous_values, updated_by, updated_at
139
+ FROM rebase.entity_history
140
+ WHERE table_name = ${tableName}
141
+ AND entity_id = ${String(entityId)}
142
+ ORDER BY updated_at DESC
143
+ LIMIT ${limit}
144
+ OFFSET ${offset}
145
+ `)
146
+ ]);
147
+
148
+ const total = parseInt(
149
+ (countResult.rows[0] as Record<string, string>)?.count ?? "0",
150
+ 10
151
+ );
152
+
153
+ return {
154
+ data: dataResult.rows as unknown as HistoryEntry[],
155
+ total
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Fetch a single history entry by ID.
161
+ */
162
+ async fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null> {
163
+ const result = await this.db.execute(sql`
164
+ SELECT id, table_name, entity_id, action, changed_fields,
165
+ "values", previous_values, updated_by, updated_at
166
+ FROM rebase.entity_history
167
+ WHERE id = ${historyId}
168
+ `);
169
+
170
+ if (result.rows.length === 0) return null;
171
+ return result.rows[0] as unknown as HistoryEntry;
172
+ }
173
+
174
+ // ───────── Retention / Pruning ─────────
175
+
176
+ /**
177
+ * Prune history for a single entity: enforce maxEntries and TTL.
178
+ */
179
+ async pruneEntity(tableName: string, entityId: string): Promise<number> {
180
+ let deleted = 0;
181
+
182
+ // 1. TTL — delete entries older than ttlDays
183
+ const ttlResult = await this.db.execute(sql`
184
+ DELETE FROM rebase.entity_history
185
+ WHERE table_name = ${tableName}
186
+ AND entity_id = ${String(entityId)}
187
+ AND updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})
188
+ `);
189
+ deleted += ttlResult.rowCount ?? 0;
190
+
191
+ // 2. Max entries — keep the newest maxEntries, delete the rest
192
+ const maxResult = await this.db.execute(sql`
193
+ DELETE FROM rebase.entity_history
194
+ WHERE id IN (
195
+ SELECT id FROM rebase.entity_history
196
+ WHERE table_name = ${tableName}
197
+ AND entity_id = ${String(entityId)}
198
+ ORDER BY updated_at DESC
199
+ OFFSET ${this.retention.maxEntries}
200
+ )
201
+ `);
202
+ deleted += maxResult.rowCount ?? 0;
203
+
204
+ return deleted;
205
+ }
206
+
207
+ /**
208
+ * Global prune: enforce TTL across ALL entities in a single sweep.
209
+ * Intended to be called periodically (e.g. once per hour or daily).
210
+ */
211
+ async pruneExpired(): Promise<number> {
212
+ const result = await this.db.execute(sql`
213
+ DELETE FROM rebase.entity_history
214
+ WHERE updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})
215
+ `);
216
+ return result.rowCount ?? 0;
217
+ }
218
+ }
219
+
220
+
221
+ /**
222
+ * Shallow comparison to find top-level keys that changed between two objects.
223
+ */
224
+ export function findChangedFields(
225
+ oldValues: Record<string, unknown>,
226
+ newValues: Record<string, unknown>
227
+ ): string[] | null {
228
+ const changed: string[] = [];
229
+ const allKeys = new Set([
230
+ ...Object.keys(oldValues),
231
+ ...Object.keys(newValues)
232
+ ]);
233
+
234
+ for (const key of allKeys) {
235
+ const oldVal = oldValues[key];
236
+ const newVal = newValues[key];
237
+
238
+ // Skip internal metadata
239
+ if (key.startsWith("__")) continue;
240
+
241
+ if (oldVal !== newVal) {
242
+ // For objects/arrays, use JSON comparison
243
+ if (
244
+ typeof oldVal === "object" && oldVal !== null &&
245
+ typeof newVal === "object" && newVal !== null
246
+ ) {
247
+ if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
248
+ changed.push(key);
249
+ }
250
+ } else {
251
+ changed.push(key);
252
+ }
253
+ }
254
+ }
255
+
256
+ return changed.length > 0 ? changed : null;
257
+ }
@@ -0,0 +1,45 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+
4
+ /**
5
+ * Auto-create the entity history table if it doesn't exist.
6
+ * This runs on startup when history is enabled, following the same
7
+ * pattern as `ensureAuthTablesExist`.
8
+ */
9
+ export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {
10
+ console.log("🔍 Checking entity history table...");
11
+
12
+ try {
13
+ // Create the rebase schema (idempotent — may already exist from auth init)
14
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
15
+
16
+ await db.execute(sql`
17
+ CREATE TABLE IF NOT EXISTS rebase.entity_history (
18
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
19
+ table_name TEXT NOT NULL,
20
+ entity_id TEXT NOT NULL,
21
+ action TEXT NOT NULL,
22
+ changed_fields TEXT[],
23
+ "values" JSONB,
24
+ previous_values JSONB,
25
+ updated_by TEXT,
26
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
27
+ )
28
+ `);
29
+
30
+ await db.execute(sql`
31
+ CREATE INDEX IF NOT EXISTS idx_history_entity
32
+ ON rebase.entity_history(table_name, entity_id)
33
+ `);
34
+
35
+ await db.execute(sql`
36
+ CREATE INDEX IF NOT EXISTS idx_history_time
37
+ ON rebase.entity_history(table_name, entity_id, updated_at DESC)
38
+ `);
39
+
40
+ console.log("✅ Entity history table ready");
41
+ } catch (error) {
42
+ console.error("❌ Failed to create entity history table:", error);
43
+ console.warn("⚠️ Continuing without creating history table.");
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./connection";
2
+ export * from "./interfaces";
3
+ export * from "./PostgresBackendDriver";
4
+ export * from "./databasePoolManager";
5
+ export * from "./schema/auth-schema";
6
+ export * from "./schema/generate-drizzle-schema-logic";
7
+ export * from "./schema/generate-drizzle-schema";
8
+ export * from "./utils/drizzle-conditions";
9
+ export * from "./services/realtimeService";
10
+ export * from "./websocket";
11
+ export * from "./collections/PostgresCollectionRegistry";
12
+ export * from "./services/BranchService";
13
+ export * from "./PostgresBootstrapper";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Database Abstraction Interfaces
3
+ *
4
+ * These interfaces define the contracts that any database backend must implement
5
+ * to be used with Rebase. This allows for pluggable database backends like
6
+ * PostgreSQL, MongoDB, MySQL, etc.
7
+ */
8
+
9
+ import {
10
+ Entity,
11
+ EntityCollection,
12
+ FilterValues,
13
+ WhereFilterOp,
14
+ DatabaseConnection,
15
+ QueryFilter,
16
+ FetchCollectionOptions,
17
+ SearchOptions,
18
+ CountOptions,
19
+ ConditionBuilder,
20
+ ConditionBuilderStatic,
21
+ EntityRepository,
22
+ CollectionSubscriptionConfig,
23
+ EntitySubscriptionConfig,
24
+ RealtimeProvider,
25
+ CollectionRegistryInterface,
26
+ DataTransformer,
27
+ BackendConfig,
28
+ BackendInstance,
29
+ BackendFactory
30
+ } from "@rebasepro/types";
31
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
32
+ import { PgTransaction } from "drizzle-orm/pg-core";
33
+
34
+ /**
35
+ * Type representing either a direct database connection or a transaction.
36
+ * Used to allow services to operate within a transaction context.
37
+ * Note: `any` is intentional here — it represents a Drizzle client with
38
+ * a dynamic schema, enabling `db.query[tableName]` access without casts.
39
+ */
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ export type DrizzleClient = NodePgDatabase<any> | PgTransaction<any, any, any>;
42
+
43
+ export type {
44
+ DatabaseConnection,
45
+ QueryFilter,
46
+ FetchCollectionOptions,
47
+ SearchOptions,
48
+ CountOptions,
49
+ ConditionBuilder,
50
+ ConditionBuilderStatic,
51
+ EntityRepository,
52
+ CollectionSubscriptionConfig,
53
+ EntitySubscriptionConfig,
54
+ RealtimeProvider,
55
+ CollectionRegistryInterface,
56
+ DataTransformer,
57
+ BackendConfig,
58
+ BackendInstance,
59
+ BackendFactory
60
+ };
@@ -0,0 +1,146 @@
1
+ import { pgSchema, varchar, uuid, timestamp, boolean, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
2
+ import { relations } from "drizzle-orm";
3
+
4
+ /**
5
+ * Dedicated PostgreSQL schema for all Rebase internal tables.
6
+ * Keeps the user's `public` schema clean.
7
+ */
8
+ export const rebaseSchema = pgSchema("rebase");
9
+
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
+ provider: varchar("provider", { length: 50 }).notNull().default("email"), // 'email' | 'google'
20
+ googleId: varchar("google_id", { length: 255 }).unique(),
21
+ emailVerified: boolean("email_verified").default(false).notNull(),
22
+ emailVerificationToken: varchar("email_verification_token", { length: 255 }),
23
+ emailVerificationSentAt: timestamp("email_verification_sent_at"),
24
+ createdAt: timestamp("created_at").defaultNow().notNull(),
25
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
26
+ });
27
+
28
+ /**
29
+ * Roles table - defines permission sets
30
+ */
31
+ export const roles = rebaseSchema.table("roles", {
32
+ id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
33
+ name: varchar("name", { length: 100 }).notNull(),
34
+ isAdmin: boolean("is_admin").default(false).notNull(),
35
+ defaultPermissions: jsonb("default_permissions").$type<{
36
+ read?: boolean;
37
+ create?: boolean;
38
+ edit?: boolean;
39
+ delete?: boolean;
40
+ }>(),
41
+ collectionPermissions: jsonb("collection_permissions").$type<
42
+ Record<string, {
43
+ read?: boolean;
44
+ create?: boolean;
45
+ edit?: boolean;
46
+ delete?: boolean;
47
+ }>
48
+ >(),
49
+ config: jsonb("config").$type<{
50
+ createCollections?: boolean;
51
+ editCollections?: "own" | "all" | boolean;
52
+ deleteCollections?: "own" | "all" | boolean;
53
+ }>()
54
+ });
55
+
56
+ /**
57
+ * User-Role junction table
58
+ */
59
+ export const userRoles = rebaseSchema.table("user_roles", {
60
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
61
+ roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
62
+ }, (table) => ({
63
+ pk: primaryKey({ columns: [table.userId, table.roleId] })
64
+ }));
65
+
66
+ /**
67
+ * Refresh tokens for long-lived sessions
68
+ */
69
+ export const refreshTokens = rebaseSchema.table("refresh_tokens", {
70
+ id: uuid("id").defaultRandom().primaryKey(),
71
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
72
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
73
+ expiresAt: timestamp("expires_at").notNull(),
74
+ userAgent: varchar("user_agent", { length: 500 }),
75
+ ipAddress: varchar("ip_address", { length: 45 }),
76
+ createdAt: timestamp("created_at").defaultNow().notNull()
77
+ }, (table) => ({
78
+ uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
79
+ }));
80
+
81
+ /**
82
+ * Password reset tokens for forgot password flow
83
+ */
84
+ export const passwordResetTokens = rebaseSchema.table("password_reset_tokens", {
85
+ id: uuid("id").defaultRandom().primaryKey(),
86
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
87
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
88
+ expiresAt: timestamp("expires_at").notNull(),
89
+ usedAt: timestamp("used_at"),
90
+ createdAt: timestamp("created_at").defaultNow().notNull()
91
+ });
92
+
93
+ /**
94
+ * App config - key/value store for custom settings
95
+ */
96
+ export const appConfig = rebaseSchema.table("app_config", {
97
+ key: varchar("key", { length: 100 }).primaryKey(),
98
+ value: jsonb("value").notNull(),
99
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
100
+ });
101
+
102
+ // Relations
103
+ export const usersRelations = relations(users, ({ many }) => ({
104
+ userRoles: many(userRoles),
105
+ refreshTokens: many(refreshTokens),
106
+ passwordResetTokens: many(passwordResetTokens)
107
+ }));
108
+
109
+ export const rolesRelations = relations(roles, ({ many }) => ({
110
+ userRoles: many(userRoles)
111
+ }));
112
+
113
+ export const userRolesRelations = relations(userRoles, ({ one }) => ({
114
+ user: one(users, {
115
+ fields: [userRoles.userId],
116
+ references: [users.id]
117
+ }),
118
+ role: one(roles, {
119
+ fields: [userRoles.roleId],
120
+ references: [roles.id]
121
+ })
122
+ }));
123
+
124
+ export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
125
+ user: one(users, {
126
+ fields: [refreshTokens.userId],
127
+ references: [users.id]
128
+ })
129
+ }));
130
+
131
+ export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
132
+ user: one(users, {
133
+ fields: [passwordResetTokens.userId],
134
+ references: [users.id]
135
+ })
136
+ }));
137
+
138
+ // Type exports
139
+ export type User = typeof users.$inferSelect;
140
+ export type NewUser = typeof users.$inferInsert;
141
+ export type Role = typeof roles.$inferSelect;
142
+ export type NewRole = typeof roles.$inferInsert;
143
+ export type UserRole = typeof userRoles.$inferSelect;
144
+ export type RefreshToken = typeof refreshTokens.$inferSelect;
145
+ export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
146
+ export type AppConfig = typeof appConfig.$inferSelect;