@rebasepro/server-postgresql 0.6.1 → 0.8.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/dist/PostgresBackendDriver.d.ts +8 -0
- package/dist/auth/services.d.ts +21 -1
- package/dist/cli-errors.d.ts +29 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +2 -2
- package/dist/index.es.js +2987 -230
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-default-policies.d.ts +12 -0
- package/dist/schema/auth-schema.d.ts +227 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/services/entityService.d.ts +1 -1
- package/dist/utils/pg-error-utils.d.ts +10 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/package.json +15 -9
- package/src/PostgresBackendDriver.ts +200 -68
- package/src/PostgresBootstrapper.ts +34 -2
- 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 +264 -245
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/data-transformer.ts +2 -2
- package/src/schema/auth-default-policies.ts +97 -0
- package/src/schema/auth-schema.ts +25 -2
- package/src/schema/doctor.ts +2 -4
- package/src/schema/generate-drizzle-schema-logic.ts +20 -82
- package/src/schema/generate-drizzle-schema.ts +3 -5
- package/src/schema/generate-postgres-ddl-logic.ts +487 -0
- package/src/schema/generate-postgres-ddl.ts +116 -0
- package/src/services/EntityPersistService.ts +10 -8
- package/src/services/entityService.ts +28 -3
- package/src/services/realtimeService.ts +26 -2
- 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/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/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-callbacks-redaction.test.ts +86 -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-error-utils.test.ts +50 -1
- package/test/postgresDataDriver.test.ts +2 -1
- package/test/realtimeService-channels.test.ts +696 -0
- package/test/unmapped-tables-safety.test.ts +55 -342
- package/vitest.e2e.config.ts +10 -0
package/src/data-transformer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { eq, SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn } from "drizzle-orm/pg-core";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
-
import {
|
|
4
|
+
import { EntityCollection, Properties, Property, Relation, RelationProperty, Vector, BinaryProperty } from "@rebasepro/types";
|
|
5
5
|
import { getTableName, resolveCollectionRelations, findRelation, createRelationRef, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "@rebasepro/common";
|
|
6
6
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
|
|
7
7
|
import { DrizzleConditionBuilder } from "./utils/drizzle-conditions";
|
|
@@ -551,7 +551,7 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
|
|
|
551
551
|
relationDef = findRelation(resolvedRelations, propertyKey);
|
|
552
552
|
}
|
|
553
553
|
if (!relationDef) {
|
|
554
|
-
relationDef =
|
|
554
|
+
relationDef = collection.relations?.find((rel: Relation) => rel.relationName === (property as RelationProperty).relationName);
|
|
555
555
|
}
|
|
556
556
|
|
|
557
557
|
if (!relationDef) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { EntityCollection, SecurityRule, SecurityOperation, AuthCollectionConfig, PolicyExpression, isPostgresCollection, policy } 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
|
+
// Expressed structurally (not as raw SQL) so the admin UI can evaluate it
|
|
41
|
+
// exactly — the framework's most security-critical policy must be reflected
|
|
42
|
+
// precisely, not left as an un-evaluable raw clause. Compiles to
|
|
43
|
+
// `auth.uid() IS NULL OR (string_to_array(auth.roles(), ',') && ARRAY['admin'])`.
|
|
44
|
+
const ADMIN_WRITE_EXPR: PolicyExpression = policy.or(
|
|
45
|
+
policy.not(policy.authenticated()),
|
|
46
|
+
policy.rolesOverlap(["admin"])
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** Write operations that must be admin-gated by default on auth collections. */
|
|
50
|
+
const DEFAULT_GUARDED_OPS: SecurityOperation[] = ["insert", "update", "delete"];
|
|
51
|
+
|
|
52
|
+
/** Whether a collection is flagged as an authentication collection. */
|
|
53
|
+
function isAuthCollection(collection: EntityCollection): boolean {
|
|
54
|
+
const auth = collection.auth;
|
|
55
|
+
return auth === true || (typeof auth === "object" && (auth as AuthCollectionConfig)?.enabled === true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the security rules that should be applied to a collection: the
|
|
60
|
+
* author's explicit `securityRules`, plus, for auth collections, an
|
|
61
|
+
* auto-injected restrictive admin gate and permissive admin grant on every
|
|
62
|
+
* write operation. Together these guarantee only admins (or the trusted server
|
|
63
|
+
* context) can write the row.
|
|
64
|
+
*
|
|
65
|
+
* Non-auth collections, and auth collections that opt out via
|
|
66
|
+
* `disableDefaultAuthPolicies`, are returned unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export function getEffectiveSecurityRules(collection: EntityCollection): SecurityRule[] {
|
|
69
|
+
const explicit = (isPostgresCollection(collection) ? collection.securityRules : undefined) ?? [];
|
|
70
|
+
|
|
71
|
+
if (!isAuthCollection(collection) || collection.disableDefaultAuthPolicies) {
|
|
72
|
+
return explicit;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tableName = getTableName(collection);
|
|
76
|
+
|
|
77
|
+
// Restrictive gate: AND'd with all other policies, so no permissive rule
|
|
78
|
+
// (e.g. an owner "edit your own row" rule) can let a non-admin through.
|
|
79
|
+
const requireAdminGate: SecurityRule = {
|
|
80
|
+
name: `${tableName}_require_admin_write`,
|
|
81
|
+
mode: "restrictive",
|
|
82
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
83
|
+
condition: ADMIN_WRITE_EXPR,
|
|
84
|
+
check: ADMIN_WRITE_EXPR
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Permissive grant: a restrictive policy alone denies everything, so this
|
|
88
|
+
// grants the baseline write to admins / the server context.
|
|
89
|
+
const allowAdminWrite: SecurityRule = {
|
|
90
|
+
name: `${tableName}_default_admin_write`,
|
|
91
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
92
|
+
condition: ADMIN_WRITE_EXPR,
|
|
93
|
+
check: ADMIN_WRITE_EXPR
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return [...explicit, allowAdminWrite, requireAdminGate];
|
|
97
|
+
}
|
|
@@ -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);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollection, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty } from "@rebasepro/types";
|
|
2
2
|
import { getPrimaryKeys } from "../services/entity-helpers";
|
|
3
|
-
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
3
|
+
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres } from "@rebasepro/common";
|
|
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}")`;
|
|
@@ -307,62 +308,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
307
308
|
};
|
|
308
309
|
|
|
309
310
|
/**
|
|
310
|
-
*
|
|
311
|
-
* The result is wrapped in a Drizzle sql`` template literal.
|
|
311
|
+
* Wraps a compiled SQL clause in a Drizzle `sql\`...\`` template literal.
|
|
312
312
|
*/
|
|
313
|
-
const
|
|
314
|
-
// Replace {column_name} with column_name directly (so Drizzle-kit can parse it as a static string)
|
|
315
|
-
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
316
|
-
return `sql\`${resolved}\``;
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Wraps a SQL clause with a role check using AND.
|
|
321
|
-
* Generates: `(<clause>) AND (string_to_array(auth.roles(), ',') && ARRAY['<role1>','<role2>'])`
|
|
322
|
-
*/
|
|
323
|
-
const wrapWithRoleCheck = (clause: string, roles: string[]): string => {
|
|
324
|
-
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
|
|
325
|
-
const roleCondition = `string_to_array(auth.roles(), ',') && ${rolesArrayString}`;
|
|
326
|
-
return `sql\`(${unwrapSql(clause)}) AND (${roleCondition})\``;
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Extracts the inner expression from a `sql\`...\`` wrapper.
|
|
331
|
-
*/
|
|
332
|
-
const unwrapSql = (sqlExpr: string): string => {
|
|
333
|
-
const match = sqlExpr.match(/^sql`(.*)`$/s);
|
|
334
|
-
return match ? match[1] : sqlExpr;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Builds the USING clause for a policy based on shortcuts or raw SQL.
|
|
339
|
-
*/
|
|
340
|
-
const buildUsingClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
|
|
341
|
-
if (rule.using) {
|
|
342
|
-
return resolveRawSql(rule.using);
|
|
343
|
-
}
|
|
344
|
-
if (rule.access === "public") {
|
|
345
|
-
return "sql`true`";
|
|
346
|
-
}
|
|
347
|
-
if (rule.ownerField) {
|
|
348
|
-
const prop = collection.properties?.[rule.ownerField];
|
|
349
|
-
const colName = resolveColumnName(rule.ownerField, prop);
|
|
350
|
-
return `sql\`${colName} = auth.uid()\``;
|
|
351
|
-
}
|
|
352
|
-
return null;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Builds the WITH CHECK clause for a policy based on shortcuts or raw SQL.
|
|
357
|
-
* Falls back to the USING clause if not explicitly provided.
|
|
358
|
-
*/
|
|
359
|
-
const buildWithCheckClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
|
|
360
|
-
if (rule.withCheck) {
|
|
361
|
-
return resolveRawSql(rule.withCheck);
|
|
362
|
-
}
|
|
363
|
-
// For insert/update/all, fall back to using clause if withCheck not specified
|
|
364
|
-
return buildUsingClause(rule, collection);
|
|
365
|
-
};
|
|
313
|
+
const wrapSql = (clause: string): string => `sql\`${clause}\``;
|
|
366
314
|
|
|
367
315
|
/**
|
|
368
316
|
* Generates a deterministic hash based on the rule configuration.
|
|
@@ -377,7 +325,9 @@ const getPolicyNameHash = (rule: SecurityRule): string => {
|
|
|
377
325
|
rol: rule.roles?.slice().sort(),
|
|
378
326
|
pg: rule.pgRoles?.slice().sort(),
|
|
379
327
|
u: rule.using,
|
|
380
|
-
w: rule.withCheck
|
|
328
|
+
w: rule.withCheck,
|
|
329
|
+
c: rule.condition,
|
|
330
|
+
ch: rule.check
|
|
381
331
|
});
|
|
382
332
|
return createHash("sha1").update(data).digest("hex").substring(0, 7);
|
|
383
333
|
};
|
|
@@ -416,7 +366,6 @@ const generatePolicyCode = (collection: EntityCollection, rule: SecurityRule, in
|
|
|
416
366
|
*/
|
|
417
367
|
const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
|
|
418
368
|
const mode = rule.mode ?? "permissive";
|
|
419
|
-
const roles = rule.roles ? [...rule.roles].sort() : undefined;
|
|
420
369
|
|
|
421
370
|
// Determine which clauses this operation needs:
|
|
422
371
|
// SELECT, DELETE → USING only
|
|
@@ -425,26 +374,13 @@ const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRu
|
|
|
425
374
|
const needsUsing = operation !== "insert";
|
|
426
375
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
427
376
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
usingClause = wrapWithRoleCheck(usingClause, roles);
|
|
436
|
-
} else if (needsUsing) {
|
|
437
|
-
// Roles-only rule (e.g. { operation: "select", roles: ["admin"] })
|
|
438
|
-
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
|
|
439
|
-
usingClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
|
|
440
|
-
}
|
|
441
|
-
if (withCheckClause) {
|
|
442
|
-
withCheckClause = wrapWithRoleCheck(withCheckClause, roles);
|
|
443
|
-
} else if (needsWithCheck) {
|
|
444
|
-
const rolesArrayString = `ARRAY[${roles.map(r => `'${r}'`).join(",")}]`;
|
|
445
|
-
withCheckClause = `sql\`string_to_array(auth.roles(), ',') && ${rolesArrayString}\``;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
377
|
+
// Desugar the rule (access / ownerField / roles / structured condition / raw
|
|
378
|
+
// SQL) into the shared PolicyExpression model, then compile to SQL — the same
|
|
379
|
+
// normalization the DDL generator and the client-side evaluator use.
|
|
380
|
+
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
381
|
+
|
|
382
|
+
let usingClause = needsUsing && usingExpr ? wrapSql(policyToPostgres(usingExpr, collection)) : null;
|
|
383
|
+
let withCheckClause = needsWithCheck && withCheckExpr ? wrapSql(policyToPostgres(withCheckExpr, collection)) : null;
|
|
448
384
|
|
|
449
385
|
// Fallback: if we still have no clauses, deny all (safety net)
|
|
450
386
|
if (!usingClause && needsUsing) {
|
|
@@ -616,7 +552,9 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
616
552
|
const enumVarName = getEnumVarName(collectionPath, propName);
|
|
617
553
|
const enumDbName = `${collectionPath}_${resolveColumnName(propName, prop)}`;
|
|
618
554
|
const values = Array.isArray(prop.enum)
|
|
619
|
-
? prop.enum.map(v
|
|
555
|
+
? (prop.enum as (string | number | { id: string | number })[]).map((v: string | number | { id: string | number }) =>
|
|
556
|
+
String(typeof v === "object" && v !== null && "id" in v ? v.id : v)
|
|
557
|
+
)
|
|
620
558
|
: Object.keys(prop.enum);
|
|
621
559
|
if (values.length > 0) {
|
|
622
560
|
schemaContent += `export const ${enumVarName} = pgEnum(\"${enumDbName}\", [${values.map(v => `'${v}'`).join(", ")}]);\n`;
|
|
@@ -707,8 +645,8 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
707
645
|
|
|
708
646
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
709
647
|
|
|
710
|
-
const securityRules =
|
|
711
|
-
if (!stripPolicies && securityRules
|
|
648
|
+
const securityRules = getEffectiveSecurityRules(collection);
|
|
649
|
+
if (!stripPolicies && securityRules.length > 0) {
|
|
712
650
|
schemaContent += "\n}, (table) => ([\n";
|
|
713
651
|
securityRules.forEach((rule: SecurityRule, idx: number) => {
|
|
714
652
|
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));
|