@rebasepro/server-postgres 0.12.1-canary.gdfba2a1 → 0.12.1-canary.gf5f1d39
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/dist/auth/services.d.ts +0 -16
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/{ensure-collection-tables-CIHSH962.js → ensure-collection-tables-CNTcZGvn.js} +5 -5
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +1 -0
- package/dist/index.es.js +9629 -379
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +144 -83
- package/dist/{src-DihrDFuP.js → src-BbFOPJ1S.js} +150 -160
- package/dist/src-BbFOPJ1S.js.map +1 -0
- package/dist/{src-DoU9yPqq.js → src-Zqwaw3P5.js} +159 -3
- package/dist/src-Zqwaw3P5.js.map +1 -0
- package/package.json +14 -14
- package/src/auth/ensure-tables.ts +9 -164
- package/src/auth/services.ts +2 -21
- package/src/schema/auth-schema.ts +19 -30
- package/src/schema/generate-drizzle-schema-logic.ts +3 -7
- package/src/schema/generate-postgres-ddl-logic.ts +3 -7
- package/dist/backup-service-DH9kPg-E.js +0 -8866
- package/dist/backup-service-DH9kPg-E.js.map +0 -1
- package/dist/connection-B5Wndbr1.js +0 -196
- package/dist/connection-B5Wndbr1.js.map +0 -1
- package/dist/ensure-collection-tables-CIHSH962.js.map +0 -1
- package/dist/src-DihrDFuP.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-BKcGvILX.js +0 -528
- package/dist/websocket-BKcGvILX.js.map +0 -1
|
@@ -107,38 +107,16 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
107
107
|
? "GENERATED ALWAYS AS IDENTITY"
|
|
108
108
|
: "DEFAULT gen_random_uuid()::text";
|
|
109
109
|
|
|
110
|
-
// Identifiers for the constraint and indexes reconciled further down.
|
|
111
|
-
// Derived from the resolved table name so two auth tables in different
|
|
112
|
-
// schemas cannot collide, and truncated to Postgres's 63-byte identifier
|
|
113
|
-
// limit here rather than letting the server truncate silently — the
|
|
114
|
-
// `IF NOT EXISTS` guards below have to compare against the same name
|
|
115
|
-
// Postgres actually stored, or they re-run forever.
|
|
116
|
-
const authIdentifier = (suffix: string) => `${resolvedTable}_${suffix}`.slice(0, 63);
|
|
117
|
-
const emailLengthConstraint = `"${authIdentifier("email_length_check")}"`;
|
|
118
|
-
const emailLowerUniqueIndex = authIdentifier("email_lower_key");
|
|
119
|
-
const verificationTokenIndex = authIdentifier("email_verification_token_idx");
|
|
120
|
-
|
|
121
|
-
// Every string column here is TEXT, deliberately. In Postgres VARCHAR(n)
|
|
122
|
-
// and TEXT are the same type with the same storage and the same
|
|
123
|
-
// performance; the only difference is a length check, and none of these
|
|
124
|
-
// columns wants one. The widths this table used to carry were inherited
|
|
125
|
-
// MySQL habit (255) and they were all wrong in the same direction —
|
|
126
|
-
// `password_hash VARCHAR(255)` against a 193-char scrypt string left 62
|
|
127
|
-
// characters of headroom in front of a KEY_LENGTH constant living in
|
|
128
|
-
// another package, and `photo_url VARCHAR(500)` rejected the `data:` URIs
|
|
129
|
-
// and long signed URLs that OAuth providers hand back. A limit worth
|
|
130
|
-
// having is a CHECK — alterable without a table rewrite, unlike a type
|
|
131
|
-
// modifier — which is why `email` has one and nothing else does.
|
|
132
110
|
await db.execute(sql`
|
|
133
111
|
CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
|
|
134
112
|
id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},
|
|
135
|
-
email
|
|
136
|
-
display_name
|
|
137
|
-
photo_url
|
|
113
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
114
|
+
display_name VARCHAR(255),
|
|
115
|
+
photo_url VARCHAR(500),
|
|
138
116
|
roles TEXT[] DEFAULT '{}' NOT NULL,
|
|
139
|
-
password_hash
|
|
117
|
+
password_hash VARCHAR(255),
|
|
140
118
|
email_verified BOOLEAN DEFAULT FALSE NOT NULL,
|
|
141
|
-
email_verification_token
|
|
119
|
+
email_verification_token VARCHAR(255),
|
|
142
120
|
email_verification_sent_at TIMESTAMP WITH TIME ZONE,
|
|
143
121
|
is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
|
|
144
122
|
metadata JSONB DEFAULT '{}' NOT NULL,
|
|
@@ -381,12 +359,12 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
381
359
|
// statement that references it. `email` is deliberately absent: it has
|
|
382
360
|
// existed since the first era and cannot be added NOT NULL safely.
|
|
383
361
|
const userColumnBackfills = [
|
|
384
|
-
"display_name
|
|
385
|
-
"photo_url
|
|
362
|
+
"display_name VARCHAR(255)",
|
|
363
|
+
"photo_url VARCHAR(500)",
|
|
386
364
|
"roles TEXT[] DEFAULT '{}' NOT NULL",
|
|
387
|
-
"password_hash
|
|
365
|
+
"password_hash VARCHAR(255)",
|
|
388
366
|
"email_verified BOOLEAN DEFAULT FALSE NOT NULL",
|
|
389
|
-
"email_verification_token
|
|
367
|
+
"email_verification_token VARCHAR(255)",
|
|
390
368
|
"email_verification_sent_at TIMESTAMP WITH TIME ZONE",
|
|
391
369
|
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
392
370
|
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
@@ -401,139 +379,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
401
379
|
`);
|
|
402
380
|
}
|
|
403
381
|
|
|
404
|
-
// Which of the columns below the table actually has. An adopted table —
|
|
405
|
-
// one this framework did not create, which the column-name resolution in
|
|
406
|
-
// `services.ts` exists to support — may be missing any of them, and every
|
|
407
|
-
// statement past this point has to tolerate that rather than abort the
|
|
408
|
-
// whole migration block.
|
|
409
|
-
const usersColumns = await db.execute(sql`
|
|
410
|
-
SELECT column_name, data_type
|
|
411
|
-
FROM information_schema.columns
|
|
412
|
-
WHERE table_schema = ${usersSchema} AND table_name = ${resolvedTable}
|
|
413
|
-
`);
|
|
414
|
-
const usersColumnTypes = new Map(
|
|
415
|
-
(usersColumns.rows as { column_name: string; data_type: string }[])
|
|
416
|
-
.map(row => [row.column_name, row.data_type])
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
// ── Migration: VARCHAR(n) → TEXT on the users string columns ────────
|
|
420
|
-
// Tables created before the widths came off still carry them. Postgres
|
|
421
|
-
// treats varchar(n) → text as binary-coercible with no stricter
|
|
422
|
-
// constraint, so this is a catalogue-only change: no table rewrite, no
|
|
423
|
-
// index rebuild, just a brief ACCESS EXCLUSIVE lock. Guarded on the
|
|
424
|
-
// current type so it runs once and is a pure catalogue read thereafter.
|
|
425
|
-
for (const column of ["email", "display_name", "photo_url", "password_hash", "email_verification_token"]) {
|
|
426
|
-
if (usersColumnTypes.get(column) !== "character varying") continue;
|
|
427
|
-
await db.execute(sql`
|
|
428
|
-
ALTER TABLE ${sql.raw(usersTableName)}
|
|
429
|
-
ALTER COLUMN ${sql.raw(`"${column}"`)} TYPE TEXT
|
|
430
|
-
`);
|
|
431
|
-
logger.info(`🔧 Widened ${usersTableName}.${column} from VARCHAR(n) to TEXT`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// ── Migration: case-insensitive email identity ──────────────────────
|
|
435
|
-
// `getUserByEmail` has always searched `email.toLowerCase()` while the
|
|
436
|
-
// write path stored whatever it was handed, leaving normalisation to a
|
|
437
|
-
// convention every caller had to remember. A row that reached the table
|
|
438
|
-
// with mixed case is then invisible to every lookup — the account exists,
|
|
439
|
-
// login reports no such user, and the plain UNIQUE on `email` does not
|
|
440
|
-
// stop a second row differing only in case, because it compares bytes.
|
|
441
|
-
//
|
|
442
|
-
// Fixed on both sides: `mapPayload` now folds on write, and this index
|
|
443
|
-
// makes the database agree. A unique index on lower(email) is strictly
|
|
444
|
-
// stronger than the byte-exact UNIQUE that older tables carry, so the
|
|
445
|
-
// old constraint is left alone — it can no longer fire on anything the
|
|
446
|
-
// new one would allow.
|
|
447
|
-
//
|
|
448
|
-
// Deliberately no AUTH_SCHEMA_VERSION bump: a runtime that predates this
|
|
449
|
-
// migration keeps working against the migrated table (all of its own
|
|
450
|
-
// write paths already lower-cased), which is exactly the additive case
|
|
451
|
-
// the version stamp is documented not to cover.
|
|
452
|
-
if (usersColumnTypes.has("email")) {
|
|
453
|
-
const indexPresent = await db.execute(sql`
|
|
454
|
-
SELECT 1 FROM pg_class c
|
|
455
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
456
|
-
WHERE n.nspname = ${usersSchema} AND c.relname = ${emailLowerUniqueIndex} AND c.relkind = 'i'
|
|
457
|
-
`);
|
|
458
|
-
if (indexPresent.rows.length === 0) {
|
|
459
|
-
// Case-collisions already in the table would make the unique
|
|
460
|
-
// index impossible to build. Report them and leave the table
|
|
461
|
-
// alone: the next boot retries, so fixing the rows is all the
|
|
462
|
-
// operator has to do. Failing loudly beats folding the emails
|
|
463
|
-
// and letting CREATE INDEX pick which account survives.
|
|
464
|
-
const collisions = await db.execute(sql`
|
|
465
|
-
SELECT lower(email) AS normalized, count(*)::int AS occurrences
|
|
466
|
-
FROM ${sql.raw(usersTableName)}
|
|
467
|
-
WHERE email IS NOT NULL
|
|
468
|
-
GROUP BY lower(email)
|
|
469
|
-
HAVING count(*) > 1
|
|
470
|
-
LIMIT 10
|
|
471
|
-
`);
|
|
472
|
-
if (collisions.rows.length > 0) {
|
|
473
|
-
const sample = (collisions.rows as { normalized: string; occurrences: number }[])
|
|
474
|
-
.map(row => `${row.normalized} (×${row.occurrences})`)
|
|
475
|
-
.join(", ");
|
|
476
|
-
logger.error(
|
|
477
|
-
`❌ Cannot enforce case-insensitive email uniqueness on ${usersTableName}: ` +
|
|
478
|
-
`these addresses already exist more than once, differing only in case — ${sample}. ` +
|
|
479
|
-
"Merge or delete the duplicates and restart; until then two accounts can share " +
|
|
480
|
-
"one address and only the lower-cased one is reachable by login."
|
|
481
|
-
);
|
|
482
|
-
} else {
|
|
483
|
-
const folded = await db.execute(sql`
|
|
484
|
-
UPDATE ${sql.raw(usersTableName)}
|
|
485
|
-
SET email = lower(email)
|
|
486
|
-
WHERE email IS NOT NULL AND email <> lower(email)
|
|
487
|
-
`);
|
|
488
|
-
if (folded.rowCount) {
|
|
489
|
-
logger.info(`🔧 Lower-cased ${folded.rowCount} email address(es) in ${usersTableName}`);
|
|
490
|
-
}
|
|
491
|
-
await db.execute(sql`
|
|
492
|
-
CREATE UNIQUE INDEX IF NOT EXISTS ${sql.raw(`"${emailLowerUniqueIndex}"`)}
|
|
493
|
-
ON ${sql.raw(usersTableName)} (lower(email))
|
|
494
|
-
`);
|
|
495
|
-
logger.info(`✅ Email uniqueness on ${usersTableName} is now case-insensitive`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// ── Migration: bound the email column's length ──────────────────────
|
|
501
|
-
// The only length limit on this table worth keeping. 320 is the RFC 5321
|
|
502
|
-
// maximum (64-char local part + @ + 255-char domain), and it matters here
|
|
503
|
-
// beyond tidiness: `email` carries a btree index, and a sufficiently long
|
|
504
|
-
// value fails index insertion with an error that says nothing about
|
|
505
|
-
// email. NOT VALID so an adopted table with a long row still migrates —
|
|
506
|
-
// it binds all new writes, which is the part that matters.
|
|
507
|
-
if (usersColumnTypes.has("email")) {
|
|
508
|
-
const checkPresent = await db.execute(sql`
|
|
509
|
-
SELECT 1 FROM pg_constraint c
|
|
510
|
-
JOIN pg_class t ON t.oid = c.conrelid
|
|
511
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
512
|
-
WHERE n.nspname = ${usersSchema}
|
|
513
|
-
AND t.relname = ${resolvedTable}
|
|
514
|
-
AND c.conname = ${authIdentifier("email_length_check")}
|
|
515
|
-
`);
|
|
516
|
-
if (checkPresent.rows.length === 0) {
|
|
517
|
-
await db.execute(sql`
|
|
518
|
-
ALTER TABLE ${sql.raw(usersTableName)}
|
|
519
|
-
ADD CONSTRAINT ${sql.raw(emailLengthConstraint)} CHECK (length(email) <= 320) NOT VALID
|
|
520
|
-
`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// ── Index: email verification token lookups ─────────────────────────
|
|
525
|
-
// `getUserByVerificationToken` filters on this column, which had no
|
|
526
|
-
// index — every click of a verification link was a sequential scan of
|
|
527
|
-
// the whole users table. Partial, because the column is NULL for every
|
|
528
|
-
// user who is not mid-verification, which is nearly all of them.
|
|
529
|
-
if (usersColumnTypes.has("email_verification_token")) {
|
|
530
|
-
await db.execute(sql`
|
|
531
|
-
CREATE INDEX IF NOT EXISTS ${sql.raw(`"${verificationTokenIndex}"`)}
|
|
532
|
-
ON ${sql.raw(usersTableName)} (email_verification_token)
|
|
533
|
-
WHERE email_verification_token IS NOT NULL
|
|
534
|
-
`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
382
|
// ── Migration: refresh_tokens become session-scoped, rotation-safe ──
|
|
538
383
|
// Two shapes are reconciled here, on EVERY table named refresh_tokens
|
|
539
384
|
// in whatever schema it lives (a database provisioned by an older era
|
package/src/auth/services.ts
CHANGED
|
@@ -54,25 +54,6 @@ function getColumn(table: RebasePgTable | undefined, ...keys: string[]): RebaseP
|
|
|
54
54
|
return key ? table[key] : undefined;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
* The single definition of what an email address looks like in storage.
|
|
59
|
-
*
|
|
60
|
-
* Reads have always folded case; writes did not, and normalising was left to
|
|
61
|
-
* each caller. That asymmetry is only ever one forgotten `.toLowerCase()` away
|
|
62
|
-
* from a row no lookup can find — the account exists, every sign-in path
|
|
63
|
-
* reports no such user, and the byte-exact UNIQUE on the column does not stop a
|
|
64
|
-
* duplicate differing only in case. Applied on both sides here so the guarantee
|
|
65
|
-
* belongs to the repository rather than to its callers' discipline; the
|
|
66
|
-
* `lower(email)` unique index added in `ensureAuthTablesExist` is the database
|
|
67
|
-
* half of the same rule.
|
|
68
|
-
*
|
|
69
|
-
* Whitespace goes too: a trailing space survives the fold and reproduces the
|
|
70
|
-
* problem exactly.
|
|
71
|
-
*/
|
|
72
|
-
export function normalizeEmail<T>(email: T): T | string {
|
|
73
|
-
return typeof email === "string" ? email.trim().toLowerCase() : email;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
57
|
/**
|
|
77
58
|
* PostgreSQL implementation of UserRepository.
|
|
78
59
|
* Handles all user-related database operations using Drizzle ORM.
|
|
@@ -203,7 +184,7 @@ export class UserService implements UserRepository {
|
|
|
203
184
|
const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
|
|
204
185
|
|
|
205
186
|
if ("id" in data) payload[idKey] = data.id;
|
|
206
|
-
if ("email" in data) payload[emailKey] =
|
|
187
|
+
if ("email" in data) payload[emailKey] = data.email;
|
|
207
188
|
if ("passwordHash" in data) payload[passwordHashKey] = data.passwordHash;
|
|
208
189
|
if ("displayName" in data) payload[displayNameKey] = data.displayName;
|
|
209
190
|
if ("photoUrl" in data) payload[photoUrlKey] = data.photoUrl;
|
|
@@ -263,7 +244,7 @@ export class UserService implements UserRepository {
|
|
|
263
244
|
async getUserByEmail(email: string): Promise<UserData | null> {
|
|
264
245
|
const emailCol = getColumn(this.usersTable, "email");
|
|
265
246
|
if (!emailCol) return null;
|
|
266
|
-
const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol,
|
|
247
|
+
const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, email.toLowerCase()));
|
|
267
248
|
return row ? this.mapRowToUser(row as Record<string, unknown>) : null;
|
|
268
249
|
}
|
|
269
250
|
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
import { pgSchema, pgTable, uuid, timestamp, boolean, jsonb, text, unique, index } from "drizzle-orm/pg-core";
|
|
1
|
+
import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique, index } from "drizzle-orm/pg-core";
|
|
2
2
|
import { relations } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Factory function to dynamically create the auth tables bound to the specified schema names.
|
|
6
|
-
*
|
|
7
|
-
* This module builds queries; it does not create tables. `ensureAuthTablesExist`
|
|
8
|
-
* owns the DDL, which makes everything here a *claim* about a database it cannot
|
|
9
|
-
* enforce — and the claims drifted. Every column below was declared
|
|
10
|
-
* `varchar(n)` while the DDL created it as `TEXT`: `user_agent` as varchar(500),
|
|
11
|
-
* `ip_address` as varchar(45), `secret_encrypted` as varchar(500), every
|
|
12
|
-
* `token_hash` as varchar(255). None of it was true of any database this
|
|
13
|
-
* framework ever provisioned. Harmless at runtime — drizzle does not enforce a
|
|
14
|
-
* length client-side, so the widths only ever misled the next reader — but a
|
|
15
|
-
* schema module that describes columns that do not exist is worse than no
|
|
16
|
-
* schema module. They are `text` here now because they are TEXT there.
|
|
17
6
|
*/
|
|
18
7
|
export function createAuthSchema(usersSchemaName = "rebase") {
|
|
19
8
|
const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
@@ -26,12 +15,12 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
26
15
|
*/
|
|
27
16
|
const users = usersTableCreator("users", {
|
|
28
17
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
29
|
-
email:
|
|
30
|
-
passwordHash:
|
|
31
|
-
displayName:
|
|
32
|
-
photoUrl:
|
|
18
|
+
email: varchar("email", { length: 255 }).notNull().unique(),
|
|
19
|
+
passwordHash: varchar("password_hash", { length: 255 }), // NULL for OAuth-only users
|
|
20
|
+
displayName: varchar("display_name", { length: 255 }),
|
|
21
|
+
photoUrl: varchar("photo_url", { length: 500 }),
|
|
33
22
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
34
|
-
emailVerificationToken:
|
|
23
|
+
emailVerificationToken: varchar("email_verification_token", { length: 255 }),
|
|
35
24
|
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
36
25
|
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
37
26
|
roles: text("roles").array().default([]).notNull(),
|
|
@@ -76,7 +65,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
76
65
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
77
66
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
78
67
|
sessionId: uuid("session_id").defaultRandom().notNull(),
|
|
79
|
-
tokenHash:
|
|
68
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
80
69
|
expiresAt: timestamp("expires_at").notNull(),
|
|
81
70
|
revoked: boolean("revoked").default(false).notNull(),
|
|
82
71
|
rotatedAt: timestamp("rotated_at"),
|
|
@@ -87,8 +76,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
87
76
|
* that rotates immediately after it.
|
|
88
77
|
*/
|
|
89
78
|
sessionStartedAt: timestamp("session_started_at").defaultNow().notNull(),
|
|
90
|
-
userAgent:
|
|
91
|
-
ipAddress:
|
|
79
|
+
userAgent: varchar("user_agent", { length: 500 }),
|
|
80
|
+
ipAddress: varchar("ip_address", { length: 45 }),
|
|
92
81
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
93
82
|
}, (table) => ({
|
|
94
83
|
sessionIdx: index("idx_refresh_tokens_session").on(table.sessionId)
|
|
@@ -100,7 +89,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
100
89
|
const passwordResetTokens = tableCreator("password_reset_tokens", {
|
|
101
90
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
102
91
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
103
|
-
tokenHash:
|
|
92
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
104
93
|
expiresAt: timestamp("expires_at").notNull(),
|
|
105
94
|
usedAt: timestamp("used_at"),
|
|
106
95
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
@@ -110,7 +99,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
110
99
|
* App config - key/value store for custom settings
|
|
111
100
|
*/
|
|
112
101
|
const appConfig = tableCreator("app_config", {
|
|
113
|
-
key:
|
|
102
|
+
key: varchar("key", { length: 100 }).primaryKey(),
|
|
114
103
|
value: jsonb("value").notNull(),
|
|
115
104
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
116
105
|
});
|
|
@@ -121,8 +110,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
121
110
|
const userIdentities = tableCreator("user_identities", {
|
|
122
111
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
123
112
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
124
|
-
provider:
|
|
125
|
-
providerId:
|
|
113
|
+
provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
|
|
114
|
+
providerId: varchar("provider_id", { length: 255 }).notNull(),
|
|
126
115
|
profileData: jsonb("profile_data"),
|
|
127
116
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
128
117
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -136,9 +125,9 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
136
125
|
const mfaFactors = tableCreator("mfa_factors", {
|
|
137
126
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
138
127
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
139
|
-
factorType:
|
|
140
|
-
secretEncrypted:
|
|
141
|
-
friendlyName:
|
|
128
|
+
factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
|
|
129
|
+
secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
|
|
130
|
+
friendlyName: varchar("friendly_name", { length: 255 }),
|
|
142
131
|
verified: boolean("verified").default(false).notNull(),
|
|
143
132
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
144
133
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
@@ -152,7 +141,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
152
141
|
factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
|
|
153
142
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
154
143
|
verifiedAt: timestamp("verified_at"),
|
|
155
|
-
ipAddress:
|
|
144
|
+
ipAddress: varchar("ip_address", { length: 45 }),
|
|
156
145
|
expiresAt: timestamp("expires_at").notNull()
|
|
157
146
|
});
|
|
158
147
|
|
|
@@ -162,7 +151,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
162
151
|
const recoveryCodes = tableCreator("recovery_codes", {
|
|
163
152
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
164
153
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
165
|
-
codeHash:
|
|
154
|
+
codeHash: varchar("code_hash", { length: 255 }).notNull(),
|
|
166
155
|
usedAt: timestamp("used_at"),
|
|
167
156
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
168
157
|
});
|
|
@@ -173,7 +162,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
173
162
|
const magicLinkTokens = tableCreator("magic_link_tokens", {
|
|
174
163
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
175
164
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
176
|
-
tokenHash:
|
|
165
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
177
166
|
expiresAt: timestamp("expires_at").notNull(),
|
|
178
167
|
usedAt: timestamp("used_at"),
|
|
179
168
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/collection-helpers";
|
|
3
|
-
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig
|
|
3
|
+
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
5
5
|
import { logger } from "@rebasepro/server";
|
|
6
6
|
// --- Helper Functions ---
|
|
@@ -98,13 +98,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
98
98
|
} else if (stringProp.columnType === "uuid") {
|
|
99
99
|
columnDefinition = `uuid("${colName}")`;
|
|
100
100
|
} else if (stringProp.columnType === "char") {
|
|
101
|
-
columnDefinition = `char("${colName}"
|
|
101
|
+
columnDefinition = `char("${colName}")`;
|
|
102
102
|
} else if (stringProp.columnType === "varchar") {
|
|
103
|
-
|
|
104
|
-
// no length is an UNBOUNDED varchar in Postgres, which is what
|
|
105
|
-
// this emitted while the DDL generator emitted VARCHAR(255) for
|
|
106
|
-
// the very same property.
|
|
107
|
-
columnDefinition = `varchar("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
|
|
103
|
+
columnDefinition = `varchar("${colName}")`;
|
|
108
104
|
} else {
|
|
109
105
|
// `text` is the default, and the only length-unbounded choice.
|
|
110
106
|
// Ask for `varchar` explicitly if you want the length constraint.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo } from "@rebasepro/types";
|
|
2
|
-
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig
|
|
2
|
+
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
4
4
|
|
|
5
5
|
// --- Helper Functions ---
|
|
@@ -103,15 +103,11 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
103
103
|
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") {
|
|
104
104
|
return "UUID";
|
|
105
105
|
}
|
|
106
|
-
// Width comes from `validation.max` when the property states one.
|
|
107
|
-
// It used to be a hardcoded 255 here and *absent* on the Drizzle
|
|
108
|
-
// path, so the same property produced a bounded column down one
|
|
109
|
-
// generator and an unbounded one down the other.
|
|
110
106
|
if (stringProp.columnType === "char") {
|
|
111
|
-
return
|
|
107
|
+
return "CHAR(255)";
|
|
112
108
|
}
|
|
113
109
|
if (stringProp.columnType === "varchar") {
|
|
114
|
-
return
|
|
110
|
+
return "VARCHAR(255)";
|
|
115
111
|
}
|
|
116
112
|
// `text` is the default. The two generators disagreed here before:
|
|
117
113
|
// this one emitted VARCHAR(255) while the drizzle path emitted a bare
|