@rebasepro/server-postgresql 0.3.0 → 0.5.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.
Files changed (60) hide show
  1. package/README.md +69 -89
  2. package/dist/common/src/collections/default-collections.d.ts +5 -8
  3. package/dist/common/src/data/query_builder.d.ts +6 -2
  4. package/dist/common/src/util/permissions.d.ts +14 -6
  5. package/dist/index.es.js +379 -611
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/index.umd.js +375 -607
  8. package/dist/index.umd.js.map +1 -1
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
  10. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
  11. package/dist/server-postgresql/src/auth/services.d.ts +17 -42
  12. package/dist/server-postgresql/src/data-transformer.d.ts +0 -3
  13. package/dist/server-postgresql/src/databasePoolManager.d.ts +1 -1
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
  15. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
  16. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  17. package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
  18. package/dist/server-postgresql/src/types.d.ts +3 -0
  19. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
  20. package/dist/server-postgresql/src/websocket.d.ts +8 -3
  21. package/dist/types/src/controllers/auth.d.ts +2 -2
  22. package/dist/types/src/controllers/client.d.ts +25 -40
  23. package/dist/types/src/controllers/data.d.ts +21 -3
  24. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  25. package/dist/types/src/controllers/email.d.ts +2 -0
  26. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  27. package/dist/types/src/types/backend.d.ts +38 -3
  28. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  29. package/dist/types/src/types/collections.d.ts +30 -6
  30. package/dist/types/src/types/entity_views.d.ts +19 -28
  31. package/dist/types/src/types/properties.d.ts +9 -15
  32. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  33. package/dist/types/src/users/index.d.ts +0 -1
  34. package/dist/types/src/users/user.d.ts +0 -1
  35. package/package.json +6 -6
  36. package/src/PostgresBackendDriver.ts +10 -0
  37. package/src/PostgresBootstrapper.ts +27 -22
  38. package/src/auth/ensure-tables.ts +82 -129
  39. package/src/auth/services.ts +99 -197
  40. package/src/cli.ts +50 -23
  41. package/src/data-transformer.ts +57 -95
  42. package/src/databasePoolManager.ts +2 -1
  43. package/src/schema/auth-schema.ts +13 -69
  44. package/src/schema/doctor.ts +44 -3
  45. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  46. package/src/schema/generate-drizzle-schema.ts +2 -6
  47. package/src/schema/introspect-db-logic.ts +7 -0
  48. package/src/services/EntityFetchService.ts +13 -1
  49. package/src/services/EntityPersistService.ts +38 -12
  50. package/src/services/entityService.ts +7 -0
  51. package/src/types.ts +4 -0
  52. package/src/utils/drizzle-conditions.ts +40 -5
  53. package/src/websocket.ts +38 -25
  54. package/test/auth-services.test.ts +7 -150
  55. package/test/doctor.test.ts +6 -2
  56. package/test/relation-pipeline-gaps.test.ts +315 -0
  57. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  58. package/dist/types/src/users/roles.d.ts +0 -14
  59. package/drizzle.test.config.ts +0 -10
  60. package/src/schema/default-collections.ts +0 -69
@@ -272,41 +272,19 @@ export function serializePropertyToServer(value: unknown, property: Property): u
272
272
  return value;
273
273
  }
274
274
 
275
- case "binary":
276
- if (typeof value === "string") {
277
- if (value.startsWith("data:application/octet-stream;base64,")) {
278
- const base64Data = value.split(",")[1];
279
- if (base64Data) {
280
- return Buffer.from(base64Data, "base64");
281
- }
282
- }
283
- }
284
- if (Buffer.isBuffer(value)) {
285
- return value;
286
- }
275
+ case "binary": {
276
+ const decoded = tryDecodeBase64DataUrl(value);
277
+ if (decoded) return decoded;
278
+ if (Buffer.isBuffer(value)) return value;
287
279
  return value;
280
+ }
288
281
 
289
282
  case "string":
290
- if (typeof value === "string") {
291
- if (value.startsWith("data:application/octet-stream;base64,")) {
292
- const base64Data = value.split(",")[1];
293
- if (base64Data) {
294
- return Buffer.from(base64Data, "base64");
295
- }
296
- }
297
- }
298
- return value;
299
-
300
- default:
301
- if (typeof value === "string") {
302
- if (value.startsWith("data:application/octet-stream;base64,")) {
303
- const base64Data = value.split(",")[1];
304
- if (base64Data) {
305
- return Buffer.from(base64Data, "base64");
306
- }
307
- }
308
- }
283
+ default: {
284
+ const decoded = tryDecodeBase64DataUrl(value);
285
+ if (decoded) return decoded;
309
286
  return value;
287
+ }
310
288
  }
311
289
  }
312
290
 
@@ -486,24 +464,53 @@ export async function parseDataFromServer<M extends Record<string, unknown>>(
486
464
  }
487
465
 
488
466
  /**
489
- * Parse a single property value from database format to frontend format
467
+ * Try to decode a `data:application/octet-stream;base64,...` data URL string
468
+ * into a Buffer. Returns null if the value is not a matching data URL.
490
469
  */
491
- export function parsePropertyFromServer(value: unknown, property: Property, collection: EntityCollection, propertyKey?: string): unknown {
492
- if (value === null || value === undefined) {
493
- return value;
470
+ function tryDecodeBase64DataUrl(value: unknown): Buffer | null {
471
+ if (typeof value !== "string") return null;
472
+ if (!value.startsWith("data:application/octet-stream;base64,")) return null;
473
+ const base64Data = value.split(",")[1];
474
+ return base64Data ? Buffer.from(base64Data, "base64") : null;
475
+ }
476
+
477
+ /**
478
+ * Try to resolve an unknown value into a Buffer.
479
+ * Handles native Buffers and `{ type: "Buffer", data: number[] }` objects (from JSON deserialization).
480
+ * Returns null if the value is not a buffer.
481
+ */
482
+ function tryResolveBuffer(value: unknown): Buffer | null {
483
+ if (Buffer.isBuffer(value)) return value;
484
+ if (typeof value === "object" && value !== null) {
485
+ const rawVal = value as Record<string, unknown>;
486
+ if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
487
+ return Buffer.from(rawVal.data as number[]);
488
+ }
494
489
  }
490
+ return null;
491
+ }
492
+
493
+ /**
494
+ * Convert a Buffer to a UTF-8 string if all bytes are printable ASCII,
495
+ * otherwise return a base64 data URL.
496
+ */
497
+ function bufferToStringOrBase64(buf: Buffer): string {
498
+ for (let i = 0; i < buf.length; i++) {
499
+ const b = buf[i];
500
+ // Allow standard printable ASCII + common whitespace (\r, \n, \t)
501
+ if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
502
+ return `data:application/octet-stream;base64,${buf.toString("base64")}`;
503
+ }
504
+ }
505
+ return buf.toString("utf8");
506
+ }
507
+
508
+ export function parsePropertyFromServer(value: unknown, property: Property, collection: EntityCollection, propertyKey?: string): unknown {
509
+ if (value === null || value === undefined) return value;
495
510
 
496
511
  switch (property.type) {
497
512
  case "binary": {
498
- let buf: Buffer | null = null;
499
- if (Buffer.isBuffer(value)) {
500
- buf = value;
501
- } else if (typeof value === "object" && value !== null) {
502
- const rawVal = value as Record<string, unknown>;
503
- if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
504
- buf = Buffer.from(rawVal.data as number[]);
505
- }
506
- }
513
+ const buf = tryResolveBuffer(value);
507
514
  if (buf) {
508
515
  return `data:application/octet-stream;base64,${buf.toString("base64")}`;
509
516
  }
@@ -514,32 +521,9 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
514
521
  if (typeof value === "string") return value;
515
522
 
516
523
  // Handle Buffer objects (e.g. from PostgreSQL bytea columns)
517
- let isBuffer = false;
518
- let buf: Buffer | null = null;
519
-
520
- if (Buffer.isBuffer(value)) {
521
- isBuffer = true;
522
- buf = value;
523
- } else if (typeof value === "object" && value !== null) {
524
- const rawVal = value as Record<string, unknown>;
525
- if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
526
- isBuffer = true;
527
- buf = Buffer.from(rawVal.data as number[]);
528
- }
529
- }
530
-
531
- if (isBuffer && buf) {
532
- // Heuristic: if all bytes are printable ASCII, return utf8, else base64
533
- let isPrintable = true;
534
- for (let i = 0; i < buf.length; i++) {
535
- const b = buf[i];
536
- // Allow standard printable ASCII + common whitespace (\r, \n, \t)
537
- if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
538
- isPrintable = false;
539
- break;
540
- }
541
- }
542
- return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
524
+ const buf = tryResolveBuffer(value);
525
+ if (buf) {
526
+ return bufferToStringOrBase64(buf);
543
527
  }
544
528
 
545
529
  if (typeof value === "object" && value !== null) {
@@ -665,32 +649,10 @@ export function parsePropertyFromServer(value: unknown, property: Property, coll
665
649
 
666
650
  default: {
667
651
  // Fallback for buffers in case they are mapped to something other than string
668
- let isBuffer = false;
669
- let buf: Buffer | null = null;
670
-
671
- if (Buffer.isBuffer(value)) {
672
- isBuffer = true;
673
- buf = value;
674
- } else if (typeof value === "object" && value !== null) {
675
- const rawVal = value as Record<string, unknown>;
676
- if (rawVal.type === "Buffer" && Array.isArray(rawVal.data)) {
677
- isBuffer = true;
678
- buf = Buffer.from(rawVal.data as number[]);
679
- }
680
- }
681
-
682
- if (isBuffer && buf) {
683
- let isPrintable = true;
684
- for (let i = 0; i < buf.length; i++) {
685
- const b = buf[i];
686
- if ((b < 32 || b > 126) && b !== 9 && b !== 10 && b !== 13) {
687
- isPrintable = false;
688
- break;
689
- }
690
- }
691
- return isPrintable ? buf.toString("utf8") : `data:application/octet-stream;base64,${buf.toString("base64")}`;
652
+ const buf = tryResolveBuffer(value);
653
+ if (buf) {
654
+ return bufferToStringOrBase64(buf);
692
655
  }
693
-
694
656
  return value;
695
657
  }
696
658
  }
@@ -18,7 +18,7 @@ export class DatabasePoolManager {
18
18
  }
19
19
  }
20
20
 
21
- public getDrizzle(databaseName: string): NodePgDatabase<any> {
21
+ public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {
22
22
  const existing = this.drizzleInstances.get(databaseName);
23
23
  if (existing) {
24
24
  return existing;
@@ -81,5 +81,6 @@ export class DatabasePoolManager {
81
81
  }
82
82
  await Promise.all(promises);
83
83
  this.pools.clear();
84
+ this.drizzleInstances.clear();
84
85
  }
85
86
  }
@@ -1,15 +1,14 @@
1
- import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
1
+ import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique } from "drizzle-orm/pg-core";
2
2
  import { relations } from "drizzle-orm";
3
3
 
4
4
  /**
5
5
  * Factory function to dynamically create the auth tables bound to the specified schema names.
6
6
  */
7
- export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchemaName: string = "rebase") {
8
- const rolesSchema = rolesSchemaName === "public" ? null : pgSchema(rolesSchemaName);
7
+ export function createAuthSchema(usersSchemaName: string = "rebase") {
9
8
  const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
10
9
 
11
- const rolesTableCreator = (rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable) as typeof pgTable;
12
- const usersTableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
10
+ const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
11
+ const usersTableCreator = tableCreator;
13
12
 
14
13
  /**
15
14
  * Users table - stores both email/password and OAuth users
@@ -24,48 +23,18 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
24
23
  emailVerificationToken: varchar("email_verification_token", { length: 255 }),
25
24
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
26
25
  isAnonymous: boolean("is_anonymous").default(false).notNull(),
26
+ roles: text("roles").array().default([]).notNull(),
27
27
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
28
28
  createdAt: timestamp("created_at").defaultNow().notNull(),
29
29
  updatedAt: timestamp("updated_at").defaultNow().notNull()
30
30
  });
31
31
 
32
- /**
33
- * Roles table - defines permission sets
34
- */
35
- const roles = rolesTableCreator("roles", {
36
- id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
37
- name: varchar("name", { length: 100 }).notNull(),
38
- isAdmin: boolean("is_admin").default(false).notNull(),
39
- defaultPermissions: jsonb("default_permissions").$type<{
40
- read?: boolean;
41
- create?: boolean;
42
- edit?: boolean;
43
- delete?: boolean;
44
- }>(),
45
- collectionPermissions: jsonb("collection_permissions").$type<
46
- Record<string, {
47
- read?: boolean;
48
- create?: boolean;
49
- edit?: boolean;
50
- delete?: boolean;
51
- }>
52
- >()
53
- });
54
32
 
55
- /**
56
- * User-Role junction table
57
- */
58
- const userRoles = rolesTableCreator("user_roles", {
59
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
60
- roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
61
- }, (table) => ({
62
- pk: primaryKey({ columns: [table.userId, table.roleId] })
63
- }));
64
33
 
65
34
  /**
66
35
  * Refresh tokens for long-lived sessions
67
36
  */
68
- const refreshTokens = rolesTableCreator("refresh_tokens", {
37
+ const refreshTokens = tableCreator("refresh_tokens", {
69
38
  id: uuid("id").defaultRandom().primaryKey(),
70
39
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
71
40
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
@@ -80,7 +49,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
80
49
  /**
81
50
  * Password reset tokens for forgot password flow
82
51
  */
83
- const passwordResetTokens = rolesTableCreator("password_reset_tokens", {
52
+ const passwordResetTokens = tableCreator("password_reset_tokens", {
84
53
  id: uuid("id").defaultRandom().primaryKey(),
85
54
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
86
55
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
@@ -92,7 +61,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
92
61
  /**
93
62
  * App config - key/value store for custom settings
94
63
  */
95
- const appConfig = rolesTableCreator("app_config", {
64
+ const appConfig = tableCreator("app_config", {
96
65
  key: varchar("key", { length: 100 }).primaryKey(),
97
66
  value: jsonb("value").notNull(),
98
67
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -101,7 +70,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
101
70
  /**
102
71
  * User identities - maps external OAuth profiles back to local users
103
72
  */
104
- const userIdentities = rolesTableCreator("user_identities", {
73
+ const userIdentities = tableCreator("user_identities", {
105
74
  id: uuid("id").defaultRandom().primaryKey(),
106
75
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
107
76
  provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
@@ -116,7 +85,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
116
85
  /**
117
86
  * MFA factors table - stores enrolled MFA methods
118
87
  */
119
- const mfaFactors = rolesTableCreator("mfa_factors", {
88
+ const mfaFactors = tableCreator("mfa_factors", {
120
89
  id: uuid("id").defaultRandom().primaryKey(),
121
90
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
122
91
  factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
@@ -130,7 +99,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
130
99
  /**
131
100
  * MFA challenges table - tracks active MFA verification attempts
132
101
  */
133
- const mfaChallenges = rolesTableCreator("mfa_challenges", {
102
+ const mfaChallenges = tableCreator("mfa_challenges", {
134
103
  id: uuid("id").defaultRandom().primaryKey(),
135
104
  factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
136
105
  createdAt: timestamp("created_at").defaultNow().notNull(),
@@ -142,7 +111,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
142
111
  /**
143
112
  * Recovery codes table - backup codes for MFA
144
113
  */
145
- const recoveryCodes = rolesTableCreator("recovery_codes", {
114
+ const recoveryCodes = tableCreator("recovery_codes", {
146
115
  id: uuid("id").defaultRandom().primaryKey(),
147
116
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
148
117
  codeHash: varchar("code_hash", { length: 255 }).notNull(),
@@ -151,11 +120,8 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
151
120
  });
152
121
 
153
122
  return {
154
- rolesSchema,
155
123
  usersSchema,
156
124
  users,
157
- roles,
158
- userRoles,
159
125
  refreshTokens,
160
126
  passwordResetTokens,
161
127
  appConfig,
@@ -167,14 +133,11 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
167
133
  }
168
134
 
169
135
  // Instantiate default schema and tables using the default "rebase" schema
170
- const defaultAuthSchema = createAuthSchema("rebase", "rebase");
136
+ const defaultAuthSchema = createAuthSchema("rebase");
171
137
 
172
- export const rebaseSchema = defaultAuthSchema.rolesSchema;
173
138
  export const usersSchema = defaultAuthSchema.usersSchema;
174
139
 
175
140
  export const users = defaultAuthSchema.users;
176
- export const roles = defaultAuthSchema.roles;
177
- export const userRoles = defaultAuthSchema.userRoles;
178
141
  export const refreshTokens = defaultAuthSchema.refreshTokens;
179
142
  export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
180
143
  export const appConfig = defaultAuthSchema.appConfig;
@@ -185,7 +148,6 @@ export const recoveryCodes = defaultAuthSchema.recoveryCodes;
185
148
 
186
149
  // Relations
187
150
  export const usersRelations = relations(users, ({ many }) => ({
188
- userRoles: many(userRoles),
189
151
  refreshTokens: many(refreshTokens),
190
152
  passwordResetTokens: many(passwordResetTokens),
191
153
  userIdentities: many(userIdentities),
@@ -193,21 +155,6 @@ export const usersRelations = relations(users, ({ many }) => ({
193
155
  recoveryCodes: many(recoveryCodes)
194
156
  }));
195
157
 
196
- export const rolesRelations = relations(roles, ({ many }) => ({
197
- userRoles: many(userRoles)
198
- }));
199
-
200
- export const userRolesRelations = relations(userRoles, ({ one }) => ({
201
- user: one(users, {
202
- fields: [userRoles.userId],
203
- references: [users.id]
204
- }),
205
- role: one(roles, {
206
- fields: [userRoles.roleId],
207
- references: [roles.id]
208
- })
209
- }));
210
-
211
158
  export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
212
159
  user: one(users, {
213
160
  fields: [refreshTokens.userId],
@@ -254,9 +201,6 @@ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
254
201
  // Type exports
255
202
  export type User = typeof users.$inferSelect;
256
203
  export type NewUser = typeof users.$inferInsert;
257
- export type Role = typeof roles.$inferSelect;
258
- export type NewRole = typeof roles.$inferInsert;
259
- export type UserRole = typeof userRoles.$inferSelect;
260
204
  export type RefreshToken = typeof refreshTokens.$inferSelect;
261
205
  export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
262
206
  export type AppConfig = typeof appConfig.$inferSelect;
@@ -84,10 +84,28 @@ export function getExpectedColumnType(prop: Property): string | null {
84
84
  if (dp.columnType === "time") return "time without time zone";
85
85
  return "timestamp with time zone";
86
86
  }
87
- case "map":
88
87
  case "array": {
89
- const ap = prop as ArrayProperty | MapProperty;
90
- if (ap.columnType === "json") return "json";
88
+ const ap = prop as ArrayProperty;
89
+ let colType = ap.columnType;
90
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
91
+ const ofProp = ap.of as Property;
92
+ if (ofProp.type === "string") {
93
+ colType = "text[]";
94
+ } else if (ofProp.type === "number") {
95
+ colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
96
+ } else if (ofProp.type === "boolean") {
97
+ colType = "boolean[]";
98
+ }
99
+ }
100
+
101
+ if (colType === "json") return "json";
102
+ if (colType === "jsonb") return "jsonb";
103
+ if (colType && colType.endsWith("[]")) return "ARRAY";
104
+ return "jsonb";
105
+ }
106
+ case "map": {
107
+ const mp = prop as MapProperty;
108
+ if (mp.columnType === "json") return "json";
91
109
  return "jsonb";
92
110
  }
93
111
  case "relation":
@@ -485,6 +503,29 @@ export async function checkCollectionsVsDatabase(
485
503
  if (prop.type === "vector" && dbCol.udt_name !== "vector") {
486
504
  isMismatch = true;
487
505
  }
506
+ if (prop.type === "array") {
507
+ const ap = prop as ArrayProperty;
508
+ let expectedColType = ap.columnType;
509
+ if (!expectedColType && ap.of && !Array.isArray(ap.of)) {
510
+ const ofProp = ap.of as Property;
511
+ if (ofProp.type === "string") expectedColType = "text[]";
512
+ else if (ofProp.type === "number") expectedColType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
513
+ else if (ofProp.type === "boolean") expectedColType = "boolean[]";
514
+ }
515
+ if (expectedColType && expectedColType.endsWith("[]")) {
516
+ if (actualType !== "ARRAY") {
517
+ isMismatch = true;
518
+ } else {
519
+ const expectedUdt = expectedColType === "text[]" ? "_text" :
520
+ expectedColType === "integer[]" ? "_int4" :
521
+ expectedColType === "boolean[]" ? "_bool" :
522
+ expectedColType === "numeric[]" ? "_numeric" : "";
523
+ if (expectedUdt && dbCol.udt_name !== expectedUdt) {
524
+ isMismatch = true;
525
+ }
526
+ }
527
+ }
528
+ }
488
529
  if (isMismatch) {
489
530
  issues.push({
490
531
  severity: "warning",
@@ -70,6 +70,8 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
70
70
  columnDefinition = `${enumName}("${colName}")`;
71
71
  } else if ("isId" in stringProp && stringProp.isId === "uuid") {
72
72
  columnDefinition = `uuid("${colName}")`;
73
+ } else if (stringProp.columnType === "uuid") {
74
+ columnDefinition = `uuid("${colName}")`;
73
75
  } else if (stringProp.columnType === "text") {
74
76
  columnDefinition = `text("${colName}")`;
75
77
  } else if (stringProp.columnType === "char") {
@@ -145,11 +147,39 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
145
147
  }
146
148
  break;
147
149
  }
148
- case "map":
150
+ case "map": {
151
+ const mapProp = prop as MapProperty;
152
+ if (mapProp.columnType === "json") {
153
+ columnDefinition = `json("${colName}")`;
154
+ } else {
155
+ columnDefinition = `jsonb("${colName}")`;
156
+ }
157
+ break;
158
+ }
149
159
  case "array": {
150
- const arrayOrMapProp = prop as ArrayProperty | MapProperty;
151
- if (arrayOrMapProp.columnType === "json") {
160
+ const arrayProp = prop as ArrayProperty;
161
+ let colType = arrayProp.columnType;
162
+ if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
163
+ const ofProp = arrayProp.of as Property;
164
+ if (ofProp.type === "string") {
165
+ colType = "text[]";
166
+ } else if (ofProp.type === "number") {
167
+ colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
168
+ } else if (ofProp.type === "boolean") {
169
+ colType = "boolean[]";
170
+ }
171
+ }
172
+
173
+ if (colType === "json") {
152
174
  columnDefinition = `json("${colName}")`;
175
+ } else if (colType === "text[]") {
176
+ columnDefinition = `text("${colName}").array()`;
177
+ } else if (colType === "integer[]") {
178
+ columnDefinition = `integer("${colName}").array()`;
179
+ } else if (colType === "boolean[]") {
180
+ columnDefinition = `boolean("${colName}").array()`;
181
+ } else if (colType === "numeric[]") {
182
+ columnDefinition = `numeric("${colName}").array()`;
153
183
  } else {
154
184
  columnDefinition = `jsonb("${colName}")`;
155
185
  }
@@ -5,7 +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 "@rebasepro/common";
8
+
9
9
 
10
10
  // --- Helper Functions ---
11
11
 
@@ -90,11 +90,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
90
90
  collections = [];
91
91
  }
92
92
 
93
- // Always inject defaults first; developer collections override via generic dedup
94
- // Map keyed by slug — last-write-wins, so developer collections overwrite defaults
95
- collections = Array.from(
96
- new Map([defaultUsersCollection, ...collections].map(c => [c.slug, c])).values()
97
- );
93
+
98
94
 
99
95
  // Sort collections by slug alphabetically to ensure deterministic schema generation
100
96
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
@@ -589,9 +589,16 @@ export function generateCollectionFile(
589
589
  // Array/Map heuristics (Fallback if not inferred)
590
590
  if (finalPropType === "array" && !inferenceExtra.includes("of: {")) {
591
591
  let innerType = "string";
592
+ let colType = "";
592
593
  if (col.udt_name.startsWith("_")) {
593
594
  const baseType = col.udt_name.substring(1);
594
595
  innerType = mapPgType(baseType);
596
+ if (innerType === "string") colType = "text[]";
597
+ else if (innerType === "number") colType = col.udt_name === "_numeric" ? "numeric[]" : "integer[]";
598
+ else if (innerType === "boolean") colType = "boolean[]";
599
+ }
600
+ if (colType) {
601
+ extra += `\n columnType: "${colType}",`;
595
602
  }
596
603
  extra += `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
597
604
  } else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
@@ -1,6 +1,6 @@
1
1
  import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
- import { Entity, EntityCollection, FilterValues, Relation } from "@rebasepro/types";
3
+ import { Entity, EntityCollection, FilterValues, Relation, LogicalCondition } from "@rebasepro/types";
4
4
  import type { VectorSearchParams } from "@rebasepro/types";
5
5
  import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
6
6
  import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
@@ -465,6 +465,7 @@ export class EntityFetchService {
465
465
  offset?: number;
466
466
  startAfter?: Record<string, unknown>;
467
467
  searchString?: string;
468
+ logical?: LogicalCondition;
468
469
  },
469
470
  collectionPath: string,
470
471
  withConfig?: Record<string, unknown>
@@ -494,6 +495,11 @@ export class EntityFetchService {
494
495
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
495
496
  }
496
497
 
498
+ if (options.logical) {
499
+ const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
500
+ if (logicalCondition) allConditions.push(logicalCondition);
501
+ }
502
+
497
503
  // Cursor-based pagination (startAfter)
498
504
  if (options.startAfter) {
499
505
  const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
@@ -700,6 +706,7 @@ export class EntityFetchService {
700
706
  searchString?: string;
701
707
  databaseId?: string;
702
708
  vectorSearch?: VectorSearchParams;
709
+ logical?: LogicalCondition;
703
710
  } = {}
704
711
  ): Promise<Entity<M>[]> {
705
712
  const collection = getCollectionByPath(collectionPath, this.registry);
@@ -774,6 +781,11 @@ export class EntityFetchService {
774
781
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
775
782
  }
776
783
 
784
+ if (options.logical) {
785
+ const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
786
+ if (logicalCondition) allConditions.push(logicalCondition);
787
+ }
788
+
777
789
  // Vector distance threshold filter
778
790
  if (vectorMeta?.filter) {
779
791
  allConditions.push(vectorMeta.filter);