@rebasepro/server-postgresql 0.1.0 → 0.2.1
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 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1250 -1665
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1196 -1611
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +21 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +57 -8
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +44 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +68 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +85 -8
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +17 -0
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
- package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
- package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
- package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
- package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
- package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
- package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
- package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
package/src/schema/doctor-cli.ts
CHANGED
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "path";
|
|
7
7
|
import chalk from "chalk";
|
|
8
|
+
import fs from "fs";
|
|
8
9
|
import { runDoctor } from "./doctor";
|
|
9
10
|
|
|
10
11
|
async function main() {
|
|
11
12
|
const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
|
|
12
13
|
const schemaArg = process.argv.find((a) => a.startsWith("--schema="));
|
|
14
|
+
const sdkArg = process.argv.find((a) => a.startsWith("--sdk="));
|
|
13
15
|
|
|
14
|
-
const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "
|
|
16
|
+
const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "config", "collections");
|
|
15
17
|
const schemaPath = schemaArg?.split("=")[1] ?? path.join("src", "schema.generated.ts");
|
|
18
|
+
const sdkPath = sdkArg?.split("=")[1] ?? path.join("..", "generated", "sdk", "database.types.ts");
|
|
16
19
|
|
|
17
20
|
// Load .env
|
|
18
21
|
try {
|
|
@@ -32,6 +35,7 @@ async function main() {
|
|
|
32
35
|
const report = await runDoctor({
|
|
33
36
|
collectionsPath: path.resolve(process.cwd(), collectionsPath),
|
|
34
37
|
schemaPath: path.resolve(process.cwd(), schemaPath),
|
|
38
|
+
sdkPath: path.resolve(process.cwd(), sdkPath),
|
|
35
39
|
databaseUrl: databaseUrl ?? undefined
|
|
36
40
|
});
|
|
37
41
|
|
package/src/schema/doctor.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { pathToFileURL } from "url";
|
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import { EntityCollection, isPostgresCollection, Property, NumberProperty, StringProperty, DateProperty, ArrayProperty, MapProperty, RelationProperty } from "@rebasepro/types";
|
|
16
16
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
17
|
+
import { generateTypedefs } from "@rebasepro/sdk-generator";
|
|
17
18
|
import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
18
19
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
19
20
|
|
|
@@ -35,7 +36,7 @@ export type IssueSeverity = "error" | "warning" | "info";
|
|
|
35
36
|
|
|
36
37
|
export interface DoctorIssue {
|
|
37
38
|
severity: IssueSeverity;
|
|
38
|
-
category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key";
|
|
39
|
+
category: "missing_table" | "missing_column" | "type_mismatch" | "missing_constraint" | "schema_stale" | "missing_enum" | "enum_value_mismatch" | "missing_foreign_key" | "sdk_stale";
|
|
39
40
|
table?: string;
|
|
40
41
|
column?: string;
|
|
41
42
|
expected?: string;
|
|
@@ -46,6 +47,7 @@ export interface DoctorIssue {
|
|
|
46
47
|
|
|
47
48
|
export interface DoctorReport {
|
|
48
49
|
collectionsToSchema: { passed: boolean; issues: DoctorIssue[] };
|
|
50
|
+
collectionsToSdk: { passed: boolean; issues: DoctorIssue[] };
|
|
49
51
|
schemaToDatabase: { passed: boolean; issues: DoctorIssue[] };
|
|
50
52
|
summary: { passed: number; warnings: number; errors: number };
|
|
51
53
|
}
|
|
@@ -92,6 +94,10 @@ export function getExpectedColumnType(prop: Property): string | null {
|
|
|
92
94
|
return null; // FK columns are derived from the relation, not from the property
|
|
93
95
|
case "reference":
|
|
94
96
|
return "character varying"; // References default to varchar FK
|
|
97
|
+
case "vector":
|
|
98
|
+
return "USER-DEFINED";
|
|
99
|
+
case "binary":
|
|
100
|
+
return "bytea";
|
|
95
101
|
default:
|
|
96
102
|
return null;
|
|
97
103
|
}
|
|
@@ -139,6 +145,9 @@ export async function loadCollections(collectionsPath: string): Promise<EntityCo
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
// Sort collections by slug alphabetically to ensure deterministic comparison
|
|
149
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
150
|
+
|
|
142
151
|
return collections;
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -203,6 +212,56 @@ issues };
|
|
|
203
212
|
issues };
|
|
204
213
|
}
|
|
205
214
|
|
|
215
|
+
export async function checkCollectionsVsSdk(
|
|
216
|
+
collections: EntityCollection[],
|
|
217
|
+
sdkFilePath: string
|
|
218
|
+
): Promise<{ passed: boolean; issues: DoctorIssue[] }> {
|
|
219
|
+
const issues: DoctorIssue[] = [];
|
|
220
|
+
|
|
221
|
+
// Check if SDK file exists
|
|
222
|
+
if (!fs.existsSync(sdkFilePath)) {
|
|
223
|
+
issues.push({
|
|
224
|
+
severity: "warning",
|
|
225
|
+
category: "sdk_stale",
|
|
226
|
+
message: `Generated SDK typedefs file does not exist at "${sdkFilePath}".`,
|
|
227
|
+
fix: "Run `rebase generate-sdk`"
|
|
228
|
+
});
|
|
229
|
+
return { passed: false, issues };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const expectedSdk = generateTypedefs(collections);
|
|
234
|
+
const actualSdk = await fsPromises.readFile(sdkFilePath, "utf-8");
|
|
235
|
+
|
|
236
|
+
// Normalize whitespace for comparison
|
|
237
|
+
const normalize = (s: string) =>
|
|
238
|
+
s
|
|
239
|
+
.replace(/\/\/.*$/gm, "") // strip single-line comments
|
|
240
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // strip multi-line comments
|
|
241
|
+
.replace(/\s+/g, " ")
|
|
242
|
+
.trim();
|
|
243
|
+
|
|
244
|
+
if (normalize(expectedSdk) !== normalize(actualSdk)) {
|
|
245
|
+
issues.push({
|
|
246
|
+
severity: "warning",
|
|
247
|
+
category: "sdk_stale",
|
|
248
|
+
message: "Generated SDK types are out of date — collection definitions have changed since last SDK generation.",
|
|
249
|
+
fix: "Run `rebase generate-sdk`"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch (err: unknown) {
|
|
253
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
254
|
+
issues.push({
|
|
255
|
+
severity: "warning",
|
|
256
|
+
category: "sdk_stale",
|
|
257
|
+
message: `Could not regenerate SDK types for comparison: ${message}`,
|
|
258
|
+
fix: "Run `rebase generate-sdk` to verify"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { passed: issues.length === 0, issues };
|
|
263
|
+
}
|
|
264
|
+
|
|
206
265
|
// ── Phase 2: Collections ↔ Database ──────────────────────────────────────
|
|
207
266
|
|
|
208
267
|
interface DbColumn {
|
|
@@ -383,15 +442,19 @@ export async function checkCollectionsVsDatabase(
|
|
|
383
442
|
const expectedType = getExpectedColumnType(prop);
|
|
384
443
|
if (expectedType) {
|
|
385
444
|
const actualType = dbCol.data_type;
|
|
386
|
-
|
|
445
|
+
let isMismatch = actualType !== expectedType;
|
|
446
|
+
if (prop.type === "vector" && dbCol.udt_name !== "vector") {
|
|
447
|
+
isMismatch = true;
|
|
448
|
+
}
|
|
449
|
+
if (isMismatch) {
|
|
387
450
|
issues.push({
|
|
388
451
|
severity: "warning",
|
|
389
452
|
category: "type_mismatch",
|
|
390
453
|
table: tableName,
|
|
391
454
|
column: colName,
|
|
392
|
-
expected: expectedType,
|
|
393
|
-
actual: actualType,
|
|
394
|
-
message: `Column "${colName}" in table "${tableName}": expected type "${expectedType}" but found "${actualType}".`,
|
|
455
|
+
expected: prop.type === "vector" ? "vector" : expectedType,
|
|
456
|
+
actual: dbCol.udt_name === "vector" ? "vector" : actualType,
|
|
457
|
+
message: `Column "${colName}" in table "${tableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
|
|
395
458
|
fix: "Review collection property type or run a migration"
|
|
396
459
|
});
|
|
397
460
|
}
|
|
@@ -489,6 +552,13 @@ export function renderReport(report: DoctorReport): void {
|
|
|
489
552
|
report.schemaToDatabase.issues
|
|
490
553
|
);
|
|
491
554
|
|
|
555
|
+
// Phase 3
|
|
556
|
+
renderPhase(
|
|
557
|
+
"Collections → SDK Types",
|
|
558
|
+
report.collectionsToSdk.passed,
|
|
559
|
+
report.collectionsToSdk.issues
|
|
560
|
+
);
|
|
561
|
+
|
|
492
562
|
// Summary
|
|
493
563
|
console.log(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
494
564
|
const { passed, warnings, errors } = report.summary;
|
|
@@ -554,7 +624,8 @@ function formatCategory(cat: DoctorIssue["category"]): string {
|
|
|
554
624
|
schema_stale: "Stale Schema",
|
|
555
625
|
missing_enum: "Missing Enum",
|
|
556
626
|
enum_value_mismatch: "Enum Value Mismatch",
|
|
557
|
-
missing_foreign_key: "Missing Foreign Key"
|
|
627
|
+
missing_foreign_key: "Missing Foreign Key",
|
|
628
|
+
sdk_stale: "Stale SDK Types"
|
|
558
629
|
};
|
|
559
630
|
return labels[cat];
|
|
560
631
|
}
|
|
@@ -564,6 +635,7 @@ function formatCategory(cat: DoctorIssue["category"]): string {
|
|
|
564
635
|
export async function runDoctor(options: {
|
|
565
636
|
collectionsPath: string;
|
|
566
637
|
schemaPath: string;
|
|
638
|
+
sdkPath: string;
|
|
567
639
|
databaseUrl?: string;
|
|
568
640
|
}): Promise<DoctorReport> {
|
|
569
641
|
console.log("");
|
|
@@ -591,14 +663,19 @@ issues: [] };
|
|
|
591
663
|
console.log(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
|
|
592
664
|
}
|
|
593
665
|
|
|
594
|
-
|
|
666
|
+
// Phase 3: Collections ↔ SDK Types
|
|
667
|
+
console.log(chalk.gray(" Checking Collections → SDK Types..."));
|
|
668
|
+
const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);
|
|
669
|
+
|
|
670
|
+
const allIssues = [...collectionsToSchema.issues, ...schemaToDatabase.issues, ...collectionsToSdk.issues];
|
|
595
671
|
const summary = {
|
|
596
|
-
passed: [collectionsToSchema, schemaToDatabase].filter((p) => p.passed).length,
|
|
672
|
+
passed: [collectionsToSchema, schemaToDatabase, collectionsToSdk].filter((p) => p.passed).length,
|
|
597
673
|
warnings: allIssues.filter((i) => i.severity === "warning").length,
|
|
598
674
|
errors: allIssues.filter((i) => i.severity === "error").length
|
|
599
675
|
};
|
|
600
676
|
|
|
601
677
|
const report: DoctorReport = { collectionsToSchema,
|
|
678
|
+
collectionsToSdk,
|
|
602
679
|
schemaToDatabase,
|
|
603
680
|
summary };
|
|
604
681
|
renderReport(report);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityCollection, NumberProperty, Property, Relation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollection, DateProperty, ArrayProperty, MapProperty, ReferenceProperty } from "@rebasepro/types";
|
|
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
3
|
import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
|
|
4
4
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
@@ -19,23 +19,23 @@ const resolveColumnName = (propName: string, prop?: Property | null): string =>
|
|
|
19
19
|
|
|
20
20
|
const getPrimaryKeyProp = (collection: EntityCollection): { name: string, type: "string" | "number", isUuid: boolean } => {
|
|
21
21
|
if (collection.properties) {
|
|
22
|
-
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
|
|
22
|
+
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in (prop as unknown as object) && Boolean((prop as unknown as Record<string, unknown>).isId));
|
|
23
23
|
if (idPropEntry) {
|
|
24
|
-
const prop = idPropEntry[1] as Property;
|
|
25
|
-
const isUuid = prop.type === "string" && "isId" in prop && (prop as StringProperty).isId === "uuid";
|
|
24
|
+
const prop = idPropEntry[1] as unknown as Property;
|
|
25
|
+
const isUuid = prop.type === "string" && "isId" in prop && (prop as unknown as StringProperty).isId === "uuid";
|
|
26
26
|
return { name: idPropEntry[0],
|
|
27
27
|
type: prop.type === "number" ? "number" : "string",
|
|
28
28
|
isUuid };
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
// Fallback
|
|
32
|
-
const idProp = collection.properties?.["id"] as Property | undefined;
|
|
32
|
+
const idProp = collection.properties?.["id"] as unknown as Property | undefined;
|
|
33
33
|
if (idProp?.type === "number") {
|
|
34
34
|
return { name: "id",
|
|
35
35
|
type: "number",
|
|
36
36
|
isUuid: false };
|
|
37
37
|
}
|
|
38
|
-
const isUuid = idProp?.type === "string" && "isId" in idProp && (idProp as StringProperty).isId === "uuid";
|
|
38
|
+
const isUuid = idProp?.type === "string" && "isId" in idProp && (idProp as unknown as StringProperty).isId === "uuid";
|
|
39
39
|
return { name: "id",
|
|
40
40
|
type: "string",
|
|
41
41
|
isUuid: isUuid ?? false };
|
|
@@ -53,17 +53,18 @@ const isIdProperty = (propName: string, prop: Property, collection: EntityCollec
|
|
|
53
53
|
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
54
54
|
|
|
55
55
|
// We only fallback to "id" if NO property is explicitly marked with `isId: true` or a generator string
|
|
56
|
-
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
56
|
+
const hasExplicitId = Object.values(collection.properties ?? {}).some(p => "isId" in (p as unknown as object) && Boolean((p as unknown as Record<string, unknown>).isId));
|
|
57
57
|
return !hasExplicitId && propName === "id";
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCollection, collections: EntityCollection[]): string | null => {
|
|
61
|
+
|
|
61
62
|
const colName = resolveColumnName(propName, prop);
|
|
62
63
|
let columnDefinition: string;
|
|
63
64
|
|
|
64
65
|
switch (prop.type) {
|
|
65
66
|
case "string": {
|
|
66
|
-
const stringProp = prop as StringProperty;
|
|
67
|
+
const stringProp = prop as unknown as StringProperty;
|
|
67
68
|
if (stringProp.enum) {
|
|
68
69
|
const enumName = getEnumVarName(getTableName(collection), propName);
|
|
69
70
|
columnDefinition = `${enumName}("${colName}")`;
|
|
@@ -97,7 +98,7 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
97
98
|
break;
|
|
98
99
|
}
|
|
99
100
|
case "number": {
|
|
100
|
-
const numProp = prop as NumberProperty;
|
|
101
|
+
const numProp = prop as unknown as NumberProperty;
|
|
101
102
|
const isId = isIdProperty(propName, prop, collection);
|
|
102
103
|
|
|
103
104
|
let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
@@ -154,6 +155,15 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
154
155
|
}
|
|
155
156
|
break;
|
|
156
157
|
}
|
|
158
|
+
case "vector": {
|
|
159
|
+
const vp = prop as VectorProperty;
|
|
160
|
+
columnDefinition = `vector("${colName}", { dimensions: ${vp.dimensions} })`;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "binary": {
|
|
164
|
+
columnDefinition = `customType({ dataType() { return 'bytea'; } })("${colName}")`;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
157
167
|
case "relation": {
|
|
158
168
|
const refProp = prop as RelationProperty;
|
|
159
169
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
@@ -245,8 +255,8 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
245
255
|
* The result is wrapped in a Drizzle sql`` template literal.
|
|
246
256
|
*/
|
|
247
257
|
const resolveRawSql = (expression: string): string => {
|
|
248
|
-
// Replace {column_name} with
|
|
249
|
-
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) =>
|
|
258
|
+
// Replace {column_name} with column_name directly (so Drizzle-kit can parse it as a static string)
|
|
259
|
+
const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => col);
|
|
250
260
|
return `sql\`${resolved}\``;
|
|
251
261
|
};
|
|
252
262
|
|
|
@@ -271,7 +281,7 @@ const unwrapSql = (sqlExpr: string): string => {
|
|
|
271
281
|
/**
|
|
272
282
|
* Builds the USING clause for a policy based on shortcuts or raw SQL.
|
|
273
283
|
*/
|
|
274
|
-
const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
284
|
+
const buildUsingClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
|
|
275
285
|
if (rule.using) {
|
|
276
286
|
return resolveRawSql(rule.using);
|
|
277
287
|
}
|
|
@@ -279,7 +289,9 @@ const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
|
279
289
|
return "sql`true`";
|
|
280
290
|
}
|
|
281
291
|
if (rule.ownerField) {
|
|
282
|
-
|
|
292
|
+
const prop = collection.properties?.[rule.ownerField];
|
|
293
|
+
const colName = resolveColumnName(rule.ownerField, prop);
|
|
294
|
+
return `sql\`${colName} = auth.uid()\``;
|
|
283
295
|
}
|
|
284
296
|
return null;
|
|
285
297
|
};
|
|
@@ -288,12 +300,12 @@ const buildUsingClause = (rule: SecurityRule): string | null => {
|
|
|
288
300
|
* Builds the WITH CHECK clause for a policy based on shortcuts or raw SQL.
|
|
289
301
|
* Falls back to the USING clause if not explicitly provided.
|
|
290
302
|
*/
|
|
291
|
-
const buildWithCheckClause = (rule: SecurityRule): string | null => {
|
|
303
|
+
const buildWithCheckClause = (rule: SecurityRule, collection: EntityCollection): string | null => {
|
|
292
304
|
if (rule.withCheck) {
|
|
293
305
|
return resolveRawSql(rule.withCheck);
|
|
294
306
|
}
|
|
295
307
|
// For insert/update/all, fall back to using clause if withCheck not specified
|
|
296
|
-
return buildUsingClause(rule);
|
|
308
|
+
return buildUsingClause(rule, collection);
|
|
297
309
|
};
|
|
298
310
|
|
|
299
311
|
/**
|
|
@@ -324,7 +336,8 @@ const getPolicyNameHash = (rule: SecurityRule): string => {
|
|
|
324
336
|
* - operations[] array: generates one policy per operation
|
|
325
337
|
* - Combinations: roles + ownerField, roles + raw SQL, etc.
|
|
326
338
|
*/
|
|
327
|
-
const generatePolicyCode = (
|
|
339
|
+
const generatePolicyCode = (collection: EntityCollection, rule: SecurityRule, index: number): string => {
|
|
340
|
+
const tableName = getTableName(collection);
|
|
328
341
|
// Resolve operations: operations[] takes precedence over operation (singular)
|
|
329
342
|
const ops: SecurityOperation[] = rule.operations && rule.operations.length > 0
|
|
330
343
|
? rule.operations
|
|
@@ -338,14 +351,14 @@ const generatePolicyCode = (tableName: string, rule: SecurityRule, index: number
|
|
|
338
351
|
? (ops.length > 1 ? `${rule.name}_${op}` : rule.name)
|
|
339
352
|
: `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`;
|
|
340
353
|
|
|
341
|
-
return generateSinglePolicyCode(
|
|
354
|
+
return generateSinglePolicyCode(collection, rule, op, policyName);
|
|
342
355
|
}).join("");
|
|
343
356
|
};
|
|
344
357
|
|
|
345
358
|
/**
|
|
346
359
|
* Generates a single pgPolicy() call for one specific operation.
|
|
347
360
|
*/
|
|
348
|
-
const generateSinglePolicyCode = (
|
|
361
|
+
const generateSinglePolicyCode = (collection: EntityCollection, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
|
|
349
362
|
const mode = rule.mode ?? "permissive";
|
|
350
363
|
const roles = rule.roles ? [...rule.roles].sort() : undefined;
|
|
351
364
|
|
|
@@ -356,8 +369,8 @@ const generateSinglePolicyCode = (tableName: string, rule: SecurityRule, operati
|
|
|
356
369
|
const needsUsing = operation !== "insert";
|
|
357
370
|
const needsWithCheck = operation !== "select" && operation !== "delete";
|
|
358
371
|
|
|
359
|
-
let usingClause = needsUsing ? buildUsingClause(rule) : null;
|
|
360
|
-
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule) : null;
|
|
372
|
+
let usingClause = needsUsing ? buildUsingClause(rule, collection) : null;
|
|
373
|
+
let withCheckClause = needsWithCheck ? buildWithCheckClause(rule, collection) : null;
|
|
361
374
|
|
|
362
375
|
// If roles are specified, wrap existing clauses with role check,
|
|
363
376
|
// or generate a roles-only clause.
|
|
@@ -485,18 +498,41 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
485
498
|
)
|
|
486
499
|
);
|
|
487
500
|
|
|
488
|
-
const
|
|
501
|
+
const hasVector = collections.some(c =>
|
|
489
502
|
c.properties && Object.values(c.properties).some(
|
|
490
|
-
(p: Property) =>
|
|
503
|
+
(p: Property) => p.type === "vector"
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const hasBinary = collections.some(c =>
|
|
508
|
+
c.properties && Object.values(c.properties).some(
|
|
509
|
+
(p: Property) => p.type === "binary"
|
|
491
510
|
)
|
|
492
511
|
);
|
|
493
512
|
|
|
494
513
|
// Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
|
|
495
514
|
const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
|
|
496
515
|
if (hasUuid) pgCoreImports.push("uuid");
|
|
516
|
+
if (hasVector) pgCoreImports.push("vector");
|
|
517
|
+
if (hasBinary) pgCoreImports.push("customType");
|
|
518
|
+
|
|
519
|
+
const uniqueSchemas = Array.from(new Set(
|
|
520
|
+
collections.map(c => isPostgresCollection(c) ? c.schema : undefined).filter(Boolean)
|
|
521
|
+
));
|
|
522
|
+
if (uniqueSchemas.length > 0) {
|
|
523
|
+
pgCoreImports.push("pgSchema");
|
|
524
|
+
}
|
|
525
|
+
|
|
497
526
|
schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
|
|
498
527
|
schemaContent += "import { relations as drizzleRelations, sql } from 'drizzle-orm';\n\n";
|
|
499
528
|
|
|
529
|
+
uniqueSchemas.forEach(schema => {
|
|
530
|
+
schemaContent += `export const ${schema}Schema = pgSchema("${schema}");\n`;
|
|
531
|
+
});
|
|
532
|
+
if (uniqueSchemas.length > 0) {
|
|
533
|
+
schemaContent += "\n";
|
|
534
|
+
}
|
|
535
|
+
|
|
500
536
|
const exportedTableVars: string[] = [];
|
|
501
537
|
const exportedEnumVars: string[] = [];
|
|
502
538
|
const exportedRelationVars: string[] = [];
|
|
@@ -512,6 +548,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
512
548
|
collections.forEach(collection => {
|
|
513
549
|
const collectionPath = getTableName(collection);
|
|
514
550
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
551
|
+
|
|
515
552
|
if (("enum" in prop) && (prop.type === "string" || prop.type === "number") && prop.enum) {
|
|
516
553
|
const enumVarName = getEnumVarName(collectionPath, propName);
|
|
517
554
|
const enumDbName = `${collectionPath}_${resolveColumnName(propName, prop)}`;
|
|
@@ -564,6 +601,9 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
564
601
|
const tableVarName = getTableVarName(tableName);
|
|
565
602
|
if (isJunction && relation && sourceCollection && relation.through) {
|
|
566
603
|
const targetCollection = relation.target();
|
|
604
|
+
const schema = (isPostgresCollection(targetCollection) ? targetCollection.schema : undefined) || (isPostgresCollection(sourceCollection) ? sourceCollection.schema : undefined);
|
|
605
|
+
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
606
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
567
607
|
const {
|
|
568
608
|
sourceColumn,
|
|
569
609
|
targetColumn
|
|
@@ -577,14 +617,17 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
577
617
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
578
618
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
579
619
|
|
|
580
|
-
schemaContent += `export const ${tableVarName} =
|
|
620
|
+
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
581
621
|
schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
|
|
582
622
|
schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName(targetCollection))}.${targetId}, ${refOptions}),\n`;
|
|
583
623
|
schemaContent += "}, (table) => ({\n";
|
|
584
624
|
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
|
|
585
625
|
schemaContent += "}));\n\n";
|
|
586
626
|
} else if (!isJunction) {
|
|
587
|
-
|
|
627
|
+
const schema = isPostgresCollection(collection) ? collection.schema : undefined;
|
|
628
|
+
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
629
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
630
|
+
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
588
631
|
const columns = new Set<string>();
|
|
589
632
|
Object.entries(collection.properties ?? {}).forEach(([propName, prop]) => {
|
|
590
633
|
const columnString = getDrizzleColumn(propName, prop as Property, collection, collections);
|
|
@@ -605,7 +648,7 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
605
648
|
if (!stripPolicies && securityRules && securityRules.length > 0) {
|
|
606
649
|
schemaContent += "\n}, (table) => ([\n";
|
|
607
650
|
securityRules.forEach((rule: SecurityRule, idx: number) => {
|
|
608
|
-
schemaContent += generatePolicyCode(
|
|
651
|
+
schemaContent += generatePolicyCode(collection, rule, idx);
|
|
609
652
|
});
|
|
610
653
|
schemaContent += "])).enableRLS();\n\n";
|
|
611
654
|
} else {
|
|
@@ -666,8 +709,12 @@ export const generateSchema = async (collections: EntityCollection[], stripPolic
|
|
|
666
709
|
tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {\n fields: [${tableVarName}.${relation.through.sourceColumn}],\n references: [${sourceTableVar}.${sourceId}],\n relationName: \"${owningRelationName}\"\n })`);
|
|
667
710
|
|
|
668
711
|
// Target side one(): pairs with inverse table's many(junctionTable, { relationName })
|
|
669
|
-
|
|
670
|
-
|
|
712
|
+
// Always emit a relationName to avoid collisions with the source-side's owningRelationName.
|
|
713
|
+
// When no inverse relation exists on the target collection, synthesize a unique name.
|
|
714
|
+
const targetRelationName = inverseRelationName
|
|
715
|
+
? inverseRelationName
|
|
716
|
+
: `${tableName}_${relation.through.targetColumn}`;
|
|
717
|
+
tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {\n fields: [${tableVarName}.${relation.through.targetColumn}],\n references: [${targetTableVar}.${targetId}],\n relationName: "${targetRelationName}"\n })`);
|
|
671
718
|
}
|
|
672
719
|
} else {
|
|
673
720
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import chokidar from "chokidar";
|
|
6
6
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
7
|
import { EntityCollection } from "@rebasepro/types";
|
|
8
|
+
import { defaultUsersCollection } from "./default-collections";
|
|
8
9
|
|
|
9
10
|
// --- Helper Functions ---
|
|
10
11
|
|
|
@@ -84,11 +85,20 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
84
85
|
collections = imported.backendCollections || imported.collections;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// If collections directory is empty but exists, or failed to find any, we still want to inject defaults
|
|
89
|
+
if (!collections || !Array.isArray(collections)) {
|
|
90
|
+
collections = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Inject default collections if not overridden by the developer
|
|
94
|
+
const hasUsersCollection = collections.some(c => c.slug === "users");
|
|
95
|
+
if (!hasUsersCollection) {
|
|
96
|
+
collections.push(defaultUsersCollection);
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
// Sort collections by slug alphabetically to ensure deterministic schema generation
|
|
100
|
+
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
101
|
+
|
|
92
102
|
const schemaContent = await generateSchema(collections);
|
|
93
103
|
|
|
94
104
|
if (outputPath) {
|
|
@@ -17,8 +17,8 @@ export function inferPropertyFromData(
|
|
|
17
17
|
sampleValues: unknown[],
|
|
18
18
|
isPk: boolean
|
|
19
19
|
): InferenceResult {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const result: InferenceResult = {};
|
|
21
|
+
const extraLines: string[] = [];
|
|
22
22
|
|
|
23
23
|
// Filter out null/undefined for analysis
|
|
24
24
|
const validValues = sampleValues.filter(v => v !== null && v !== undefined && v !== "");
|
|
@@ -84,7 +84,7 @@ export function inferPropertyFromData(
|
|
|
84
84
|
let allNumbers = true;
|
|
85
85
|
let allStrings = true;
|
|
86
86
|
for (const v of validValues) {
|
|
87
|
-
|
|
87
|
+
const parsed = typeof v === "string" ? JSON.parse(v) : v;
|
|
88
88
|
for (const item of parsed) {
|
|
89
89
|
if (typeof item !== "number") allNumbers = false;
|
|
90
90
|
if (typeof item !== "string") allStrings = false;
|
|
@@ -99,7 +99,7 @@ export function inferPropertyFromData(
|
|
|
99
99
|
if (allObjects && validValues.length > 0) {
|
|
100
100
|
const schema: Record<string, string> = {};
|
|
101
101
|
for (const v of validValues) {
|
|
102
|
-
|
|
102
|
+
const parsed = typeof v === "string" ? JSON.parse(v) : v;
|
|
103
103
|
for (const [k, val] of Object.entries(parsed)) {
|
|
104
104
|
if (val === null || val === undefined) continue;
|
|
105
105
|
const type = typeof val;
|
|
@@ -221,7 +221,7 @@ export function inferPropertyFromData(
|
|
|
221
221
|
if (hasFileExtension) {
|
|
222
222
|
const firstVal = validValues[0] as string;
|
|
223
223
|
const lastSlash = firstVal.lastIndexOf('/');
|
|
224
|
-
|
|
224
|
+
const inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
|
|
225
225
|
extraLines.push(` storage: {\n storagePath: "${inferredStoragePath}"\n }`);
|
|
226
226
|
} else if (isUrl) {
|
|
227
227
|
if (isMedia) {
|
|
@@ -21,6 +21,7 @@ export interface TableColumn {
|
|
|
21
21
|
udt_name: string;
|
|
22
22
|
is_nullable: string;
|
|
23
23
|
column_default: string | null;
|
|
24
|
+
atttypmod: number | null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface EnumValue {
|
|
@@ -187,7 +188,7 @@ export function mapPgType(dataType: string): string {
|
|
|
187
188
|
if (dt === "json" || dt === "jsonb") return "map";
|
|
188
189
|
|
|
189
190
|
// Binary
|
|
190
|
-
if (dt === "bytea") return "
|
|
191
|
+
if (dt === "bytea") return "binary";
|
|
191
192
|
|
|
192
193
|
// Network types
|
|
193
194
|
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
@@ -548,8 +549,9 @@ export function generateCollectionFile(
|
|
|
548
549
|
// Check if this column uses a PostgreSQL enum type
|
|
549
550
|
const colEnumValues = enumMap.get(col.udt_name);
|
|
550
551
|
const isEnumColumn = col.data_type === "USER-DEFINED" && colEnumValues !== undefined;
|
|
552
|
+
const isVectorColumn = col.udt_name === "vector";
|
|
551
553
|
|
|
552
|
-
const propType = isEnumColumn ? "string" : mapPgType(col.data_type);
|
|
554
|
+
const propType = isEnumColumn ? "string" : (isVectorColumn ? "vector" : mapPgType(col.data_type));
|
|
553
555
|
let extra = "";
|
|
554
556
|
|
|
555
557
|
const colNameLower = col.column_name.toLowerCase();
|
|
@@ -633,6 +635,11 @@ export function generateCollectionFile(
|
|
|
633
635
|
}
|
|
634
636
|
}
|
|
635
637
|
|
|
638
|
+
if (finalPropType === "vector") {
|
|
639
|
+
const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;
|
|
640
|
+
extra += `\n dimensions: ${dims},`;
|
|
641
|
+
}
|
|
642
|
+
|
|
636
643
|
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !col.column_default) {
|
|
637
644
|
if (extra.includes("validation: {")) {
|
|
638
645
|
extra = extra.replace("validation: {", "validation: {\n required: true,");
|
|
@@ -99,9 +99,20 @@ async function main() {
|
|
|
99
99
|
|
|
100
100
|
// 2. Get Columns
|
|
101
101
|
const { rows: columns } = await client.query<TableColumn>(`
|
|
102
|
-
SELECT
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
SELECT
|
|
103
|
+
c.table_name,
|
|
104
|
+
c.column_name,
|
|
105
|
+
c.data_type,
|
|
106
|
+
c.udt_name,
|
|
107
|
+
c.is_nullable,
|
|
108
|
+
c.column_default,
|
|
109
|
+
(SELECT a.atttypmod FROM pg_attribute a
|
|
110
|
+
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
111
|
+
WHERE pc.relname = c.table_name
|
|
112
|
+
AND a.attname = c.column_name
|
|
113
|
+
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
114
|
+
FROM information_schema.columns c
|
|
115
|
+
WHERE c.table_schema = $1
|
|
105
116
|
`, [pgSchema]);
|
|
106
117
|
|
|
107
118
|
// 2b. Get Enum Types and their values
|
|
@@ -606,7 +606,7 @@ export class EntityFetchService {
|
|
|
606
606
|
const row = await qb.findFirst({
|
|
607
607
|
where: eq(idField, parsedId),
|
|
608
608
|
with: withConfig
|
|
609
|
-
} as
|
|
609
|
+
} as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
|
|
610
610
|
|
|
611
611
|
if (!row) return undefined;
|
|
612
612
|
|
|
@@ -729,7 +729,7 @@ export class EntityFetchService {
|
|
|
729
729
|
);
|
|
730
730
|
|
|
731
731
|
|
|
732
|
-
const results = await qb.findMany(queryOpts as
|
|
732
|
+
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
733
733
|
|
|
734
734
|
const entities = (results as Record<string, unknown>[]).map(row =>
|
|
735
735
|
this.drizzleResultToEntity<M>(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray)
|
|
@@ -1192,7 +1192,7 @@ export class EntityFetchService {
|
|
|
1192
1192
|
);
|
|
1193
1193
|
|
|
1194
1194
|
|
|
1195
|
-
const results = await qb.findMany(queryOpts as
|
|
1195
|
+
const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
|
|
1196
1196
|
|
|
1197
1197
|
const restRows = (results as Record<string, unknown>[]).map(row =>
|
|
1198
1198
|
this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray)
|
|
@@ -1306,7 +1306,7 @@ export class EntityFetchService {
|
|
|
1306
1306
|
const row = await qb.findFirst({
|
|
1307
1307
|
where: eq(idField, parsedId),
|
|
1308
1308
|
...(withConfig ? { with: withConfig } : {})
|
|
1309
|
-
} as
|
|
1309
|
+
} as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
|
|
1310
1310
|
|
|
1311
1311
|
if (!row) return null;
|
|
1312
1312
|
|
|
@@ -1516,7 +1516,7 @@ export class EntityFetchService {
|
|
|
1516
1516
|
}
|
|
1517
1517
|
|
|
1518
1518
|
|
|
1519
|
-
const results = await queryTarget.findMany(queryOpts as
|
|
1519
|
+
const results = await queryTarget.findMany(queryOpts as Parameters<NonNullable<typeof queryTarget>["findMany"]>[0]);
|
|
1520
1520
|
|
|
1521
1521
|
// Flatten the nested Drizzle results into REST format
|
|
1522
1522
|
return results.map((row: Record<string, unknown>) => {
|