@rebasepro/server-postgres 0.0.1-canary.4829d6e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/PostgresAdapter.d.ts +6 -0
- package/dist/PostgresBackendDriver.d.ts +150 -0
- package/dist/PostgresBootstrapper.d.ts +51 -0
- package/dist/auth/ensure-tables.d.ts +10 -0
- package/dist/auth/services.d.ts +250 -0
- package/dist/backup/backup-cli.d.ts +3 -0
- package/dist/backup/backup-cron.d.ts +53 -0
- package/dist/backup/backup-service.d.ts +85 -0
- package/dist/backup/index.d.ts +12 -0
- package/dist/backup/pg-tools.d.ts +110 -0
- package/dist/backup/retention.d.ts +35 -0
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
- package/dist/connection.d.ts +65 -0
- package/dist/data-transformer.d.ts +55 -0
- package/dist/databasePoolManager.d.ts +20 -0
- package/dist/history/HistoryService.d.ts +71 -0
- package/dist/history/ensure-history-table.d.ts +7 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.es.js +23535 -0
- package/dist/index.es.js.map +1 -0
- package/dist/interfaces.d.ts +18 -0
- package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/schema/auth-schema.d.ts +2376 -0
- package/dist/schema/doctor-cli.d.ts +2 -0
- package/dist/schema/doctor.d.ts +58 -0
- package/dist/schema/dynamic-tables.d.ts +31 -0
- package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/introspect-db-inference.d.ts +5 -0
- package/dist/schema/introspect-db-logic.d.ts +118 -0
- package/dist/schema/introspect-db.d.ts +1 -0
- package/dist/schema/introspect-runtime.d.ts +57 -0
- package/dist/schema/test-schema.d.ts +24 -0
- package/dist/security/policy-drift.d.ts +57 -0
- package/dist/security/rls-enforcement.d.ts +122 -0
- package/dist/services/BranchService.d.ts +47 -0
- package/dist/services/FetchService.d.ts +214 -0
- package/dist/services/PersistService.d.ts +39 -0
- package/dist/services/RelationService.d.ts +109 -0
- package/dist/services/cdc/CdcListener.d.ts +54 -0
- package/dist/services/cdc/trigger-cdc.d.ts +64 -0
- package/dist/services/collection-helpers.d.ts +38 -0
- package/dist/services/dataService.d.ts +110 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/realtimeService.d.ts +298 -0
- package/dist/src-Eh-CZosp.js +595 -0
- package/dist/src-Eh-CZosp.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/drizzle-conditions.d.ts +138 -0
- package/dist/utils/pg-array-null-patch.d.ts +16 -0
- package/dist/utils/pg-error-utils.d.ts +65 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/dist/websocket.d.ts +18 -0
- package/package.json +113 -0
- package/src/PostgresAdapter.ts +58 -0
- package/src/PostgresBackendDriver.ts +1387 -0
- package/src/PostgresBootstrapper.ts +581 -0
- package/src/auth/ensure-tables.ts +367 -0
- package/src/auth/services.ts +1321 -0
- package/src/backup/backup-cli.ts +383 -0
- package/src/backup/backup-cron.ts +189 -0
- package/src/backup/backup-service.ts +299 -0
- package/src/backup/index.ts +12 -0
- package/src/backup/pg-tools.ts +231 -0
- package/src/backup/retention.ts +75 -0
- package/src/cli-errors.ts +265 -0
- package/src/cli-helpers.ts +196 -0
- package/src/cli.ts +786 -0
- package/src/collections/PostgresCollectionRegistry.ts +103 -0
- package/src/connection.ts +166 -0
- package/src/data-transformer.ts +733 -0
- package/src/databasePoolManager.ts +87 -0
- package/src/history/HistoryService.ts +272 -0
- package/src/history/ensure-history-table.ts +46 -0
- package/src/index.ts +15 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-bootstrap-sql.ts +41 -0
- package/src/schema/auth-default-policies.ts +125 -0
- package/src/schema/auth-schema.ts +233 -0
- package/src/schema/doctor-cli.ts +113 -0
- package/src/schema/doctor.ts +733 -0
- package/src/schema/dynamic-tables.test.ts +302 -0
- package/src/schema/dynamic-tables.ts +293 -0
- package/src/schema/generate-drizzle-schema-logic.ts +850 -0
- package/src/schema/generate-drizzle-schema.ts +131 -0
- package/src/schema/generate-postgres-ddl-logic.ts +490 -0
- package/src/schema/generate-postgres-ddl.ts +92 -0
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +910 -0
- package/src/schema/introspect-db.ts +266 -0
- package/src/schema/introspect-runtime.test.ts +212 -0
- package/src/schema/introspect-runtime.ts +293 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/security/policy-drift.test.ts +122 -0
- package/src/security/policy-drift.ts +159 -0
- package/src/security/rls-enforcement.ts +295 -0
- package/src/services/BranchService.ts +251 -0
- package/src/services/FetchService.ts +1661 -0
- package/src/services/PersistService.ts +329 -0
- package/src/services/RelationService.ts +1306 -0
- package/src/services/cdc/CdcListener.ts +167 -0
- package/src/services/cdc/trigger-cdc.ts +169 -0
- package/src/services/collection-helpers.ts +151 -0
- package/src/services/dataService.ts +246 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1502 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +1162 -0
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +227 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +640 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique } from "drizzle-orm/pg-core";
|
|
2
|
+
import { relations } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory function to dynamically create the auth tables bound to the specified schema names.
|
|
6
|
+
*/
|
|
7
|
+
export function createAuthSchema(usersSchemaName = "rebase") {
|
|
8
|
+
const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
|
|
9
|
+
|
|
10
|
+
const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
|
|
11
|
+
const usersTableCreator = tableCreator;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Users table - stores both email/password and OAuth users
|
|
15
|
+
*/
|
|
16
|
+
const users = usersTableCreator("users", {
|
|
17
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
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 }),
|
|
22
|
+
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
23
|
+
emailVerificationToken: varchar("email_verification_token", { length: 255 }),
|
|
24
|
+
emailVerificationSentAt: timestamp("email_verification_sent_at"),
|
|
25
|
+
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
26
|
+
roles: text("roles").array().default([]).notNull(),
|
|
27
|
+
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
|
|
28
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
29
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Refresh tokens for long-lived sessions
|
|
35
|
+
*/
|
|
36
|
+
const refreshTokens = tableCreator("refresh_tokens", {
|
|
37
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
38
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
39
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
40
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
41
|
+
userAgent: varchar("user_agent", { length: 500 }),
|
|
42
|
+
ipAddress: varchar("ip_address", { length: 45 }),
|
|
43
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
44
|
+
}, (table) => ({
|
|
45
|
+
uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Password reset tokens for forgot password flow
|
|
50
|
+
*/
|
|
51
|
+
const passwordResetTokens = tableCreator("password_reset_tokens", {
|
|
52
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
53
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
54
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
55
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
56
|
+
usedAt: timestamp("used_at"),
|
|
57
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* App config - key/value store for custom settings
|
|
62
|
+
*/
|
|
63
|
+
const appConfig = tableCreator("app_config", {
|
|
64
|
+
key: varchar("key", { length: 100 }).primaryKey(),
|
|
65
|
+
value: jsonb("value").notNull(),
|
|
66
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* User identities - maps external OAuth profiles back to local users
|
|
71
|
+
*/
|
|
72
|
+
const userIdentities = tableCreator("user_identities", {
|
|
73
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
74
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
75
|
+
provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
|
|
76
|
+
providerId: varchar("provider_id", { length: 255 }).notNull(),
|
|
77
|
+
profileData: jsonb("profile_data"),
|
|
78
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
79
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
80
|
+
}, (table) => ({
|
|
81
|
+
uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* MFA factors table - stores enrolled MFA methods
|
|
86
|
+
*/
|
|
87
|
+
const mfaFactors = tableCreator("mfa_factors", {
|
|
88
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
89
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
90
|
+
factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
|
|
91
|
+
secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
|
|
92
|
+
friendlyName: varchar("friendly_name", { length: 255 }),
|
|
93
|
+
verified: boolean("verified").default(false).notNull(),
|
|
94
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
95
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* MFA challenges table - tracks active MFA verification attempts
|
|
100
|
+
*/
|
|
101
|
+
const mfaChallenges = tableCreator("mfa_challenges", {
|
|
102
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
103
|
+
factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
|
|
104
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
105
|
+
verifiedAt: timestamp("verified_at"),
|
|
106
|
+
ipAddress: varchar("ip_address", { length: 45 }),
|
|
107
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Recovery codes table - backup codes for MFA
|
|
112
|
+
*/
|
|
113
|
+
const recoveryCodes = tableCreator("recovery_codes", {
|
|
114
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
115
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
116
|
+
codeHash: varchar("code_hash", { length: 255 }).notNull(),
|
|
117
|
+
usedAt: timestamp("used_at"),
|
|
118
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Magic link tokens for passwordless email login
|
|
123
|
+
*/
|
|
124
|
+
const magicLinkTokens = tableCreator("magic_link_tokens", {
|
|
125
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
126
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
127
|
+
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
128
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
129
|
+
usedAt: timestamp("used_at"),
|
|
130
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
usersSchema,
|
|
135
|
+
users,
|
|
136
|
+
refreshTokens,
|
|
137
|
+
passwordResetTokens,
|
|
138
|
+
appConfig,
|
|
139
|
+
userIdentities,
|
|
140
|
+
mfaFactors,
|
|
141
|
+
mfaChallenges,
|
|
142
|
+
recoveryCodes,
|
|
143
|
+
magicLinkTokens
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Instantiate default schema and tables using the default "rebase" schema
|
|
148
|
+
const defaultAuthSchema = createAuthSchema("rebase");
|
|
149
|
+
|
|
150
|
+
export const usersSchema = defaultAuthSchema.usersSchema;
|
|
151
|
+
|
|
152
|
+
export const users = defaultAuthSchema.users;
|
|
153
|
+
export const refreshTokens = defaultAuthSchema.refreshTokens;
|
|
154
|
+
export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
|
|
155
|
+
export const appConfig = defaultAuthSchema.appConfig;
|
|
156
|
+
export const userIdentities = defaultAuthSchema.userIdentities;
|
|
157
|
+
export const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
158
|
+
export const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
159
|
+
export const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
160
|
+
export const magicLinkTokens = defaultAuthSchema.magicLinkTokens;
|
|
161
|
+
|
|
162
|
+
// Relations
|
|
163
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
164
|
+
refreshTokens: many(refreshTokens),
|
|
165
|
+
passwordResetTokens: many(passwordResetTokens),
|
|
166
|
+
userIdentities: many(userIdentities),
|
|
167
|
+
mfaFactors: many(mfaFactors),
|
|
168
|
+
recoveryCodes: many(recoveryCodes),
|
|
169
|
+
magicLinkTokens: many(magicLinkTokens)
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
|
|
173
|
+
user: one(users, {
|
|
174
|
+
fields: [refreshTokens.userId],
|
|
175
|
+
references: [users.id]
|
|
176
|
+
})
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
|
|
180
|
+
user: one(users, {
|
|
181
|
+
fields: [passwordResetTokens.userId],
|
|
182
|
+
references: [users.id]
|
|
183
|
+
})
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
|
|
187
|
+
user: one(users, {
|
|
188
|
+
fields: [userIdentities.userId],
|
|
189
|
+
references: [users.id]
|
|
190
|
+
})
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
export const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({
|
|
194
|
+
user: one(users, {
|
|
195
|
+
fields: [mfaFactors.userId],
|
|
196
|
+
references: [users.id]
|
|
197
|
+
}),
|
|
198
|
+
challenges: many(mfaChallenges)
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
export const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({
|
|
202
|
+
factor: one(mfaFactors, {
|
|
203
|
+
fields: [mfaChallenges.factorId],
|
|
204
|
+
references: [mfaFactors.id]
|
|
205
|
+
})
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
|
|
209
|
+
user: one(users, {
|
|
210
|
+
fields: [recoveryCodes.userId],
|
|
211
|
+
references: [users.id]
|
|
212
|
+
})
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
export const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({
|
|
216
|
+
user: one(users, {
|
|
217
|
+
fields: [magicLinkTokens.userId],
|
|
218
|
+
references: [users.id]
|
|
219
|
+
})
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
// Type exports
|
|
223
|
+
export type User = typeof users.$inferSelect;
|
|
224
|
+
export type NewUser = typeof users.$inferInsert;
|
|
225
|
+
export type RefreshToken = typeof refreshTokens.$inferSelect;
|
|
226
|
+
export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
|
|
227
|
+
export type AppConfig = typeof appConfig.$inferSelect;
|
|
228
|
+
export type UserIdentity = typeof userIdentities.$inferSelect;
|
|
229
|
+
export type NewUserIdentity = typeof userIdentities.$inferInsert;
|
|
230
|
+
export type MfaFactorRow = typeof mfaFactors.$inferSelect;
|
|
231
|
+
export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
|
|
232
|
+
export type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;
|
|
233
|
+
export type MagicLinkToken = typeof magicLinkTokens.$inferSelect;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for `rebase doctor`.
|
|
4
|
+
* Invoked via tsx by the server-postgres CLI plugin.
|
|
5
|
+
*/
|
|
6
|
+
import path from "path";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { runDoctor, loadCollections } from "./doctor";
|
|
10
|
+
import { checkPolicyDrift, formatPolicyDrift, hasDrift } from "../security/policy-drift";
|
|
11
|
+
import { validatePolicyPgRoles } from "../security/rls-enforcement";
|
|
12
|
+
import { logger } from "@rebasepro/server";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The RLS half of the doctor: policies actually deployed vs the ones the
|
|
16
|
+
* collections describe, plus policy roles this server could never satisfy.
|
|
17
|
+
*
|
|
18
|
+
* Returns true when something is wrong, so callers can set the exit code.
|
|
19
|
+
*/
|
|
20
|
+
async function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<boolean> {
|
|
21
|
+
if (!databaseUrl) {
|
|
22
|
+
logger.warn(chalk.yellow(" ⚠ No DATABASE_URL — skipping RLS policy checks"));
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let problems = false;
|
|
27
|
+
const { Pool } = await import("pg");
|
|
28
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
29
|
+
try {
|
|
30
|
+
const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
|
|
31
|
+
const runSql = async (text: string) => (await pool.query(text)).rows as Record<string, unknown>[];
|
|
32
|
+
|
|
33
|
+
// A policy naming a role the server never runs as filters every row, so
|
|
34
|
+
// the collection reads as empty. Report it without booting a server.
|
|
35
|
+
try {
|
|
36
|
+
await validatePolicyPgRoles(runSql, collections as never);
|
|
37
|
+
logger.info(chalk.green(" ✓ Policy roles are usable by this server"));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
problems = true;
|
|
40
|
+
logger.info("");
|
|
41
|
+
logger.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const drift = await checkPolicyDrift(pool as never, collections);
|
|
45
|
+
logger.info("");
|
|
46
|
+
if (hasDrift(drift)) {
|
|
47
|
+
problems = true;
|
|
48
|
+
logger.info(chalk.yellow(" RLS policies: database does not match your collections"));
|
|
49
|
+
logger.info(formatPolicyDrift(drift));
|
|
50
|
+
} else {
|
|
51
|
+
logger.info(chalk.green(" ✓ RLS policies match your collections"));
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.warn(chalk.yellow(" ⚠ Could not check RLS policies"), { error: err });
|
|
55
|
+
} finally {
|
|
56
|
+
await pool.end();
|
|
57
|
+
}
|
|
58
|
+
return problems;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
|
|
63
|
+
const schemaArg = process.argv.find((a) => a.startsWith("--schema="));
|
|
64
|
+
const sdkArg = process.argv.find((a) => a.startsWith("--sdk="));
|
|
65
|
+
// --policies runs only the RLS checks: no schema diff, no SDK types. Useful
|
|
66
|
+
// as a CI gate against a deployed database, where the collection files and
|
|
67
|
+
// generated schema are not what you are asking about.
|
|
68
|
+
const policiesOnly = process.argv.includes("--policies");
|
|
69
|
+
|
|
70
|
+
const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "config", "collections");
|
|
71
|
+
const schemaPath = schemaArg?.split("=")[1] ?? path.join("src", "schema.generated.ts");
|
|
72
|
+
const sdkPath = sdkArg?.split("=")[1] ?? path.join("..", "generated", "sdk", "database.types.ts");
|
|
73
|
+
|
|
74
|
+
// Load .env
|
|
75
|
+
try {
|
|
76
|
+
const dotenv = await import("dotenv");
|
|
77
|
+
const envPath = process.env.DOTENV_CONFIG_PATH;
|
|
78
|
+
if (envPath) {
|
|
79
|
+
dotenv.config({ path: envPath });
|
|
80
|
+
} else {
|
|
81
|
+
dotenv.config();
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// dotenv may not be installed
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
88
|
+
|
|
89
|
+
if (policiesOnly) {
|
|
90
|
+
// Non-zero so this can gate CI — the whole point of the flag.
|
|
91
|
+
if (await runPolicyChecks(collectionsPath, databaseUrl)) process.exit(1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const report = await runDoctor({
|
|
96
|
+
collectionsPath: path.resolve(process.cwd(), collectionsPath),
|
|
97
|
+
schemaPath: path.resolve(process.cwd(), schemaPath),
|
|
98
|
+
sdkPath: path.resolve(process.cwd(), sdkPath),
|
|
99
|
+
databaseUrl: databaseUrl ?? undefined
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const policiesDrifted = await runPolicyChecks(collectionsPath, databaseUrl);
|
|
103
|
+
|
|
104
|
+
// Exit with non-zero code if there are errors
|
|
105
|
+
if (report.summary.errors > 0 || policiesDrifted) {
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
logger.error(chalk.red(" ✗ Doctor failed"), { error: err });
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|