@rebasepro/server-postgresql 0.1.2 → 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.
Files changed (68) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/util/entities.d.ts +2 -2
  3. package/dist/common/src/util/relations.d.ts +1 -1
  4. package/dist/index.es.js +1160 -612
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1158 -610
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  10. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  11. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
  12. package/dist/server-postgresql/src/auth/services.d.ts +37 -15
  13. package/dist/server-postgresql/src/index.d.ts +1 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
  15. package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
  16. package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
  17. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
  18. package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
  19. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  20. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  21. package/dist/types/src/controllers/auth.d.ts +9 -8
  22. package/dist/types/src/controllers/client.d.ts +3 -0
  23. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  24. package/dist/types/src/types/collections.d.ts +67 -2
  25. package/dist/types/src/types/database_adapter.d.ts +94 -0
  26. package/dist/types/src/types/entity_actions.d.ts +7 -1
  27. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  28. package/dist/types/src/types/entity_views.d.ts +36 -1
  29. package/dist/types/src/types/index.d.ts +2 -0
  30. package/dist/types/src/types/plugins.d.ts +1 -1
  31. package/dist/types/src/types/properties.d.ts +24 -5
  32. package/dist/types/src/types/property_config.d.ts +6 -2
  33. package/dist/types/src/types/relations.d.ts +1 -1
  34. package/dist/types/src/types/translations.d.ts +8 -0
  35. package/dist/types/src/users/user.d.ts +5 -0
  36. package/package.json +21 -15
  37. package/src/PostgresAdapter.ts +59 -0
  38. package/src/PostgresBackendDriver.ts +57 -8
  39. package/src/PostgresBootstrapper.ts +35 -15
  40. package/src/auth/ensure-tables.ts +82 -189
  41. package/src/auth/services.ts +421 -170
  42. package/src/cli.ts +44 -13
  43. package/src/data-transformer.ts +78 -8
  44. package/src/history/HistoryService.ts +25 -2
  45. package/src/index.ts +1 -0
  46. package/src/schema/auth-schema.ts +130 -98
  47. package/src/schema/default-collections.ts +68 -0
  48. package/src/schema/doctor-cli.ts +5 -1
  49. package/src/schema/doctor.ts +85 -8
  50. package/src/schema/generate-drizzle-schema-logic.ts +74 -27
  51. package/src/schema/generate-drizzle-schema.ts +13 -3
  52. package/src/schema/introspect-db-inference.ts +5 -5
  53. package/src/schema/introspect-db-logic.ts +9 -2
  54. package/src/schema/introspect-db.ts +14 -3
  55. package/src/services/EntityFetchService.ts +5 -5
  56. package/src/services/RelationService.ts +2 -2
  57. package/src/services/entity-helpers.ts +1 -1
  58. package/src/services/realtimeService.ts +145 -136
  59. package/src/utils/drizzle-conditions.ts +16 -2
  60. package/src/websocket.ts +113 -37
  61. package/test/auth-services.test.ts +163 -74
  62. package/test/data-transformer-hardening.test.ts +57 -0
  63. package/test/data-transformer.test.ts +43 -0
  64. package/test/generate-drizzle-schema.test.ts +7 -5
  65. package/test/introspect-db-utils.test.ts +4 -1
  66. package/test/postgresDataDriver.test.ts +17 -0
  67. package/test/realtimeService.test.ts +7 -7
  68. package/test/websocket.test.ts +139 -0
@@ -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("..", "shared", "collections");
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
 
@@ -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
- if (actualType !== expectedType) {
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
- const allIssues = [...collectionsToSchema.issues, ...schemaToDatabase.issues];
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 ${table.column_name}
249
- const resolved = expression.replace(/\{(\w+)\}/g, (_, col) => `\${table.${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
- return `sql\`\${table.${rule.ownerField}} = auth.uid()\``;
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 = (tableName: string, rule: SecurityRule, index: number): string => {
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(tableName, rule, op, policyName);
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 = (tableName: string, rule: SecurityRule, operation: SecurityOperation, policyName: string): string => {
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 hasJson = collections.some(c =>
501
+ const hasVector = collections.some(c =>
489
502
  c.properties && Object.values(c.properties).some(
490
- (p: Property) => (p.type === "map" || p.type === "array") && (p as unknown as Record<string, unknown>).columnType === "json"
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} = pgTable(\"${tableName}\", {\n`;
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
- schemaContent += `export const ${tableVarName} = pgTable(\"${tableName}\", {\n`;
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(tableName, rule, idx);
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
- const targetRelName = inverseRelationName ?? owningRelationName;
670
- tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {\n fields: [${tableVarName}.${relation.through.targetColumn}],\n references: [${targetTableVar}.${targetId}],\n relationName: \"${targetRelName}\"\n })`);
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
- if (!collections || !Array.isArray(collections) || collections.length === 0) {
88
- console.error("Error: Could not find collections array or failed to load directory.");
89
- return;
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
- let result: InferenceResult = {};
21
- let extraLines: string[] = [];
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
- let parsed = typeof v === "string" ? JSON.parse(v) : v;
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
- let parsed = typeof v === "string" ? JSON.parse(v) : v;
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
- let inferredStoragePath = lastSlash > 0 ? firstVal.substring(0, lastSlash) : "files";
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 "string";
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 table_name, column_name, data_type, udt_name, is_nullable, column_default
103
- FROM information_schema.columns
104
- WHERE table_schema = $1
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 unknown as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
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 unknown as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
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 unknown as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
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 unknown as Parameters<NonNullable<typeof qb>["findFirst"]>[0]);
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 unknown as Parameters<NonNullable<typeof queryTarget>["findMany"]>[0]);
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>) => {