@rebasepro/server-postgresql 0.6.0 → 0.7.0
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/package.json +24 -18
- package/src/PostgresBackendDriver.ts +65 -44
- package/src/PostgresBootstrapper.ts +49 -20
- package/src/auth/ensure-tables.ts +56 -1
- package/src/auth/services.ts +94 -1
- package/src/cli-errors.ts +162 -0
- package/src/cli-helpers.ts +183 -0
- package/src/cli.ts +198 -251
- package/src/data-transformer.ts +9 -1
- package/src/schema/auth-default-policies.ts +90 -0
- package/src/schema/auth-schema.ts +25 -2
- package/src/schema/doctor.ts +2 -4
- package/src/schema/generate-drizzle-schema-logic.ts +4 -3
- package/src/schema/generate-drizzle-schema.ts +3 -5
- package/src/schema/generate-postgres-ddl-logic.ts +524 -0
- package/src/schema/generate-postgres-ddl.ts +116 -0
- package/src/services/EntityPersistService.ts +18 -16
- package/src/services/entityService.ts +28 -3
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +16 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +9 -0
- package/test/array-null-safety.test.ts +335 -0
- package/test/auth-default-policies.test.ts +89 -0
- package/test/cli-helpers-extended.test.ts +324 -0
- package/test/cli-helpers.test.ts +59 -0
- package/test/connection.test.ts +292 -0
- package/test/data-transformer.test.ts +53 -2
- package/test/databasePoolManager.test.ts +289 -0
- package/test/doctor-extended.test.ts +443 -0
- package/test/e2e/db-e2e.test.ts +293 -0
- package/test/e2e/pg-setup.ts +79 -0
- package/test/entity-persist-composite-keys.test.ts +451 -0
- package/test/generate-postgres-ddl-edge-cases.test.ts +716 -0
- package/test/generate-postgres-ddl.test.ts +300 -0
- package/test/mfa-service.test.ts +544 -0
- package/test/pg-array-null-patch.test.ts +65 -0
- package/test/pg-error-utils.test.ts +50 -1
- package/test/realtimeService-channels.test.ts +696 -0
- package/test/unmapped-tables-safety.test.ts +55 -342
- package/vite.config.ts +8 -6
- package/vitest.e2e.config.ts +10 -0
- package/build-errors.txt +0 -37
- package/dist/PostgresAdapter.d.ts +0 -6
- package/dist/PostgresBackendDriver.d.ts +0 -110
- package/dist/PostgresBootstrapper.d.ts +0 -46
- package/dist/auth/ensure-tables.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -231
- package/dist/cli.d.ts +0 -1
- package/dist/collections/PostgresCollectionRegistry.d.ts +0 -47
- package/dist/connection.d.ts +0 -65
- package/dist/data-transformer.d.ts +0 -55
- package/dist/databasePoolManager.d.ts +0 -20
- package/dist/history/HistoryService.d.ts +0 -71
- package/dist/history/ensure-history-table.d.ts +0 -7
- package/dist/index.d.ts +0 -14
- package/dist/index.es.js +0 -10764
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -11055
- package/dist/index.umd.js.map +0 -1
- package/dist/interfaces.d.ts +0 -18
- package/dist/schema/auth-schema.d.ts +0 -2149
- package/dist/schema/doctor-cli.d.ts +0 -2
- package/dist/schema/doctor.d.ts +0 -52
- package/dist/schema/generate-drizzle-schema-logic.d.ts +0 -2
- package/dist/schema/generate-drizzle-schema.d.ts +0 -1
- package/dist/schema/introspect-db-inference.d.ts +0 -5
- package/dist/schema/introspect-db-logic.d.ts +0 -118
- package/dist/schema/introspect-db.d.ts +0 -1
- package/dist/schema/test-schema.d.ts +0 -24
- package/dist/services/BranchService.d.ts +0 -47
- package/dist/services/EntityFetchService.d.ts +0 -214
- package/dist/services/EntityPersistService.d.ts +0 -40
- package/dist/services/RelationService.d.ts +0 -98
- package/dist/services/entity-helpers.d.ts +0 -38
- package/dist/services/entityService.d.ts +0 -110
- package/dist/services/index.d.ts +0 -4
- package/dist/services/realtimeService.d.ts +0 -220
- package/dist/types.d.ts +0 -3
- package/dist/utils/drizzle-conditions.d.ts +0 -138
- package/dist/utils/pg-error-utils.d.ts +0 -55
- package/dist/websocket.d.ts +0 -11
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { EntityCollection, SecurityRule, SecurityOperation, AuthCollectionConfig, isPostgresCollection } from "@rebasepro/types";
|
|
2
|
+
import { getTableName } from "@rebasepro/common";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default RLS for authentication collections.
|
|
6
|
+
*
|
|
7
|
+
* Users are persisted through the same write path as any other collection —
|
|
8
|
+
* the only thing that distinguishes them is the `auth` flag. That means
|
|
9
|
+
* privileged columns (most importantly `roles`) would be editable by any
|
|
10
|
+
* authenticated user unless the collection author remembers to write a correct
|
|
11
|
+
* admin-only write policy.
|
|
12
|
+
*
|
|
13
|
+
* To make this safe by default, the schema generator injects two policies for
|
|
14
|
+
* every write operation on an auth collection. Enforcement lives in the
|
|
15
|
+
* database (RLS), so it applies to every write path — REST, WebSocket, and any
|
|
16
|
+
* internal caller — not just one entry point:
|
|
17
|
+
*
|
|
18
|
+
* 1. A **restrictive** admin gate. Restrictive policies are AND'd with every
|
|
19
|
+
* other policy, so this guarantees a write is rejected unless the caller is
|
|
20
|
+
* an admin (or the trusted server context) — even if the author also wrote
|
|
21
|
+
* a permissive rule such as "a user may edit their own row". Without this,
|
|
22
|
+
* a permissive owner rule would let a user change their own `roles`.
|
|
23
|
+
*
|
|
24
|
+
* 2. A **permissive** admin grant. Restrictive policies only constrain; at
|
|
25
|
+
* least one permissive policy must also pass for the write to be allowed.
|
|
26
|
+
* This grants the baseline write so admins (and the server context) can
|
|
27
|
+
* manage users even when the collection has no other permissive write rule.
|
|
28
|
+
*
|
|
29
|
+
* Both mirror the framework's trusted-server convention: the write is allowed
|
|
30
|
+
* when there is no authenticated user context (`auth.uid() IS NULL`, used by
|
|
31
|
+
* the built-in auth flows that run as the service role) OR the caller holds the
|
|
32
|
+
* `admin` role. Net effect: only admins (or the server) can write the row, and
|
|
33
|
+
* therefore only admins can change privileged columns like `roles`.
|
|
34
|
+
*
|
|
35
|
+
* Author-provided write rules are preserved but cannot loosen this guarantee —
|
|
36
|
+
* a non-admin write is always blocked by the restrictive gate. To take full
|
|
37
|
+
* control of an auth collection's write authorization, set
|
|
38
|
+
* `disableDefaultAuthPolicies: true` on the collection.
|
|
39
|
+
*/
|
|
40
|
+
const ADMIN_WRITE_EXPR = "auth.uid() IS NULL OR string_to_array(auth.roles(), ',') && ARRAY['admin']";
|
|
41
|
+
|
|
42
|
+
/** Write operations that must be admin-gated by default on auth collections. */
|
|
43
|
+
const DEFAULT_GUARDED_OPS: SecurityOperation[] = ["insert", "update", "delete"];
|
|
44
|
+
|
|
45
|
+
/** Whether a collection is flagged as an authentication collection. */
|
|
46
|
+
function isAuthCollection(collection: EntityCollection): boolean {
|
|
47
|
+
const auth = collection.auth;
|
|
48
|
+
return auth === true || (typeof auth === "object" && (auth as AuthCollectionConfig)?.enabled === true);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the security rules that should be applied to a collection: the
|
|
53
|
+
* author's explicit `securityRules`, plus, for auth collections, an
|
|
54
|
+
* auto-injected restrictive admin gate and permissive admin grant on every
|
|
55
|
+
* write operation. Together these guarantee only admins (or the trusted server
|
|
56
|
+
* context) can write the row.
|
|
57
|
+
*
|
|
58
|
+
* Non-auth collections, and auth collections that opt out via
|
|
59
|
+
* `disableDefaultAuthPolicies`, are returned unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export function getEffectiveSecurityRules(collection: EntityCollection): SecurityRule[] {
|
|
62
|
+
const explicit = (isPostgresCollection(collection) ? collection.securityRules : undefined) ?? [];
|
|
63
|
+
|
|
64
|
+
if (!isAuthCollection(collection) || collection.disableDefaultAuthPolicies) {
|
|
65
|
+
return explicit;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tableName = getTableName(collection);
|
|
69
|
+
|
|
70
|
+
// Restrictive gate: AND'd with all other policies, so no permissive rule
|
|
71
|
+
// (e.g. an owner "edit your own row" rule) can let a non-admin through.
|
|
72
|
+
const requireAdminGate: SecurityRule = {
|
|
73
|
+
name: `${tableName}_require_admin_write`,
|
|
74
|
+
mode: "restrictive",
|
|
75
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
76
|
+
using: ADMIN_WRITE_EXPR,
|
|
77
|
+
withCheck: ADMIN_WRITE_EXPR
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Permissive grant: a restrictive policy alone denies everything, so this
|
|
81
|
+
// grants the baseline write to admins / the server context.
|
|
82
|
+
const allowAdminWrite: SecurityRule = {
|
|
83
|
+
name: `${tableName}_default_admin_write`,
|
|
84
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
85
|
+
using: ADMIN_WRITE_EXPR,
|
|
86
|
+
withCheck: ADMIN_WRITE_EXPR
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return [...explicit, allowAdminWrite, requireAdminGate];
|
|
90
|
+
}
|
|
@@ -118,6 +118,18 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
118
118
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
119
119
|
});
|
|
120
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
|
+
|
|
121
133
|
return {
|
|
122
134
|
usersSchema,
|
|
123
135
|
users,
|
|
@@ -127,7 +139,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
127
139
|
userIdentities,
|
|
128
140
|
mfaFactors,
|
|
129
141
|
mfaChallenges,
|
|
130
|
-
recoveryCodes
|
|
142
|
+
recoveryCodes,
|
|
143
|
+
magicLinkTokens
|
|
131
144
|
};
|
|
132
145
|
}
|
|
133
146
|
|
|
@@ -144,6 +157,7 @@ export const userIdentities = defaultAuthSchema.userIdentities;
|
|
|
144
157
|
export const mfaFactors = defaultAuthSchema.mfaFactors;
|
|
145
158
|
export const mfaChallenges = defaultAuthSchema.mfaChallenges;
|
|
146
159
|
export const recoveryCodes = defaultAuthSchema.recoveryCodes;
|
|
160
|
+
export const magicLinkTokens = defaultAuthSchema.magicLinkTokens;
|
|
147
161
|
|
|
148
162
|
// Relations
|
|
149
163
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
@@ -151,7 +165,8 @@ export const usersRelations = relations(users, ({ many }) => ({
|
|
|
151
165
|
passwordResetTokens: many(passwordResetTokens),
|
|
152
166
|
userIdentities: many(userIdentities),
|
|
153
167
|
mfaFactors: many(mfaFactors),
|
|
154
|
-
recoveryCodes: many(recoveryCodes)
|
|
168
|
+
recoveryCodes: many(recoveryCodes),
|
|
169
|
+
magicLinkTokens: many(magicLinkTokens)
|
|
155
170
|
}));
|
|
156
171
|
|
|
157
172
|
export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
|
|
@@ -197,6 +212,13 @@ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
|
|
|
197
212
|
})
|
|
198
213
|
}));
|
|
199
214
|
|
|
215
|
+
export const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({
|
|
216
|
+
user: one(users, {
|
|
217
|
+
fields: [magicLinkTokens.userId],
|
|
218
|
+
references: [users.id]
|
|
219
|
+
})
|
|
220
|
+
}));
|
|
221
|
+
|
|
200
222
|
// Type exports
|
|
201
223
|
export type User = typeof users.$inferSelect;
|
|
202
224
|
export type NewUser = typeof users.$inferInsert;
|
|
@@ -208,3 +230,4 @@ export type NewUserIdentity = typeof userIdentities.$inferInsert;
|
|
|
208
230
|
export type MfaFactorRow = typeof mfaFactors.$inferSelect;
|
|
209
231
|
export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
|
|
210
232
|
export type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;
|
|
233
|
+
export type MagicLinkToken = typeof magicLinkTokens.$inferSelect;
|
package/src/schema/doctor.ts
CHANGED
|
@@ -143,8 +143,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
|
|
|
143
143
|
const filePath = path.join(resolvedPath, file);
|
|
144
144
|
try {
|
|
145
145
|
const fileUrl = pathToFileURL(filePath).href;
|
|
146
|
-
const
|
|
147
|
-
const mod = await dynamicImport(fileUrl);
|
|
146
|
+
const mod = await import(fileUrl);
|
|
148
147
|
if (mod?.default) {
|
|
149
148
|
collections.push(mod.default);
|
|
150
149
|
}
|
|
@@ -156,8 +155,7 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
|
|
|
156
155
|
}
|
|
157
156
|
} else {
|
|
158
157
|
const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
|
|
159
|
-
const
|
|
160
|
-
const imported = await dynamicImport(fileUrl);
|
|
158
|
+
const imported = await import(fileUrl);
|
|
161
159
|
const loaded = imported.backendCollections || imported.collections;
|
|
162
160
|
if (Array.isArray(loaded)) {
|
|
163
161
|
collections.push(...loaded);
|
|
@@ -4,6 +4,7 @@ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelatio
|
|
|
4
4
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
6
|
import { logger } from "@rebasepro/server-core";
|
|
7
|
+
import { getEffectiveSecurityRules } from "./auth-default-policies";
|
|
7
8
|
// --- Helper Functions ---
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -98,7 +99,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
98
99
|
columnDefinition = `uuid("${colName}")`;
|
|
99
100
|
} else if (stringProp.columnType === "uuid") {
|
|
100
101
|
columnDefinition = `uuid("${colName}")`;
|
|
101
|
-
} else if (stringProp.columnType === "text") {
|
|
102
|
+
} else if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) {
|
|
102
103
|
columnDefinition = `text("${colName}")`;
|
|
103
104
|
} else if (stringProp.columnType === "char") {
|
|
104
105
|
columnDefinition = `char("${colName}")`;
|
|
@@ -707,8 +708,8 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
707
708
|
|
|
708
709
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
709
710
|
|
|
710
|
-
const securityRules =
|
|
711
|
-
if (!stripPolicies && securityRules
|
|
711
|
+
const securityRules = getEffectiveSecurityRules(collection);
|
|
712
|
+
if (!stripPolicies && securityRules.length > 0) {
|
|
712
713
|
schemaContent += "\n}, (table) => ([\n";
|
|
713
714
|
securityRules.forEach((rule: SecurityRule, idx: number) => {
|
|
714
715
|
schemaContent += generatePolicyCode(collection, rule, idx);
|
|
@@ -68,8 +68,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
68
68
|
const filePath = path.join(resolvedPath, file);
|
|
69
69
|
try {
|
|
70
70
|
const fileUrl = pathToFileURL(filePath).href;
|
|
71
|
-
const
|
|
72
|
-
const module = await dynamicImport(fileUrl);
|
|
71
|
+
const module = await import(fileUrl);
|
|
73
72
|
if (module && module.default) {
|
|
74
73
|
collections.push(module.default);
|
|
75
74
|
}
|
|
@@ -81,8 +80,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
81
80
|
}
|
|
82
81
|
} else {
|
|
83
82
|
const fileUrl = pathToFileURL(resolvedPath).href + `?t=${Date.now()}`;
|
|
84
|
-
const
|
|
85
|
-
const imported = await dynamicImport(fileUrl);
|
|
83
|
+
const imported = await import(fileUrl);
|
|
86
84
|
collections = imported.backendCollections || imported.collections;
|
|
87
85
|
}
|
|
88
86
|
|
|
@@ -101,7 +99,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
101
99
|
const outputDir = path.dirname(outputPath);
|
|
102
100
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
103
101
|
await fsPromises.writeFile(outputPath, schemaContent);
|
|
104
|
-
logger.info(
|
|
102
|
+
logger.info(`✅ Drizzle schema generated successfully at ${outputPath}`);
|
|
105
103
|
} else {
|
|
106
104
|
logger.info("✅ Drizzle schema generated successfully.");
|
|
107
105
|
logger.info(String(schemaContent));
|