@rebasepro/server-postgresql 0.2.3 → 0.2.5

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 (63) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +9 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/index.es.js +1075 -470
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1071 -466
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +3 -1
  9. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  10. package/dist/server-postgresql/src/auth/services.d.ts +48 -31
  11. package/dist/server-postgresql/src/connection.d.ts +25 -0
  12. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2135 -41
  13. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  14. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  15. package/dist/server-postgresql/src/services/entityService.d.ts +6 -0
  16. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  17. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  18. package/dist/types/src/controllers/auth.d.ts +4 -26
  19. package/dist/types/src/controllers/client.d.ts +25 -43
  20. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  21. package/dist/types/src/controllers/data.d.ts +4 -0
  22. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  23. package/dist/types/src/controllers/registry.d.ts +5 -4
  24. package/dist/types/src/rebase_context.d.ts +1 -1
  25. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  26. package/dist/types/src/types/backend.d.ts +2 -2
  27. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  28. package/dist/types/src/types/collections.d.ts +0 -4
  29. package/dist/types/src/types/component_ref.d.ts +1 -1
  30. package/dist/types/src/types/cron.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +1 -0
  32. package/dist/types/src/types/export_import.d.ts +1 -1
  33. package/dist/types/src/types/formex.d.ts +2 -2
  34. package/dist/types/src/types/properties.d.ts +9 -7
  35. package/dist/types/src/types/translations.d.ts +28 -12
  36. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  37. package/dist/types/src/users/index.d.ts +0 -1
  38. package/dist/types/src/users/user.d.ts +0 -1
  39. package/package.json +6 -6
  40. package/src/PostgresBackendDriver.ts +14 -2
  41. package/src/PostgresBootstrapper.ts +30 -20
  42. package/src/auth/ensure-tables.ts +116 -103
  43. package/src/auth/services.ts +347 -177
  44. package/src/connection.ts +77 -0
  45. package/src/data-transformer.ts +2 -2
  46. package/src/schema/auth-schema.ts +85 -75
  47. package/src/schema/doctor.ts +44 -3
  48. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  49. package/src/schema/generate-drizzle-schema.ts +6 -6
  50. package/src/schema/introspect-db-logic.ts +7 -0
  51. package/src/services/EntityFetchService.ts +69 -10
  52. package/src/services/EntityPersistService.ts +9 -0
  53. package/src/services/entityService.ts +9 -0
  54. package/src/services/realtimeService.ts +214 -2
  55. package/src/utils/drizzle-conditions.ts +74 -2
  56. package/src/websocket.ts +10 -2
  57. package/test/auth-services.test.ts +10 -166
  58. package/test/doctor.test.ts +6 -2
  59. package/test/drizzle-conditions.test.ts +168 -0
  60. package/vite.config.ts +1 -1
  61. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  62. package/dist/types/src/users/roles.d.ts +0 -22
  63. package/src/schema/default-collections.ts +0 -69
package/src/connection.ts CHANGED
@@ -82,3 +82,80 @@ export function createPostgresDatabaseConnection(
82
82
  pool,
83
83
  connectionString };
84
84
  }
85
+
86
+ /**
87
+ * Create a direct (non-pooled) connection for operations that require
88
+ * session-level features incompatible with PgBouncer transaction mode,
89
+ * such as LISTEN/NOTIFY, prepared statements, or advisory locks.
90
+ *
91
+ * Uses a smaller pool since this is only for specific use cases.
92
+ */
93
+ export function createDirectDatabaseConnection(
94
+ connectionString: string,
95
+ schema?: Record<string, unknown>,
96
+ poolConfig?: PostgresPoolConfig
97
+ ) {
98
+ const opts = {
99
+ ...DEFAULT_POOL,
100
+ max: 5,
101
+ ...poolConfig
102
+ };
103
+
104
+ const pgPoolConfig: PoolConfig = {
105
+ connectionString,
106
+ max: opts.max,
107
+ idleTimeoutMillis: opts.idleTimeoutMillis,
108
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
109
+ query_timeout: opts.queryTimeout,
110
+ statement_timeout: opts.statementTimeout,
111
+ keepAlive: opts.keepAlive,
112
+ keepAliveInitialDelayMillis: 0
113
+ };
114
+
115
+ const pool = new Pool(pgPoolConfig);
116
+
117
+ pool.on("error", (err) => {
118
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
119
+ });
120
+
121
+ const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
122
+
123
+ return { db, pool, connectionString };
124
+ }
125
+
126
+ /**
127
+ * Create a read-only connection for routing read queries to replicas.
128
+ * Uses a moderate pool size since reads are distributed across replicas.
129
+ */
130
+ export function createReadReplicaConnection(
131
+ connectionString: string,
132
+ schema?: Record<string, unknown>,
133
+ poolConfig?: PostgresPoolConfig
134
+ ) {
135
+ const opts = {
136
+ ...DEFAULT_POOL,
137
+ max: 10,
138
+ ...poolConfig
139
+ };
140
+
141
+ const pgPoolConfig: PoolConfig = {
142
+ connectionString,
143
+ max: opts.max,
144
+ idleTimeoutMillis: opts.idleTimeoutMillis,
145
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
146
+ query_timeout: opts.queryTimeout,
147
+ statement_timeout: opts.statementTimeout,
148
+ keepAlive: opts.keepAlive,
149
+ keepAliveInitialDelayMillis: 0
150
+ };
151
+
152
+ const pool = new Pool(pgPoolConfig);
153
+
154
+ pool.on("error", (err) => {
155
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
156
+ });
157
+
158
+ const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
159
+
160
+ return { db, pool, connectionString };
161
+ }
@@ -263,8 +263,8 @@ export function serializePropertyToServer(value: unknown, property: Property): u
263
263
  if (value instanceof Vector) {
264
264
  return value.value;
265
265
  }
266
- if (value && typeof value === "object" && "value" in value && Array.isArray((value as any).value)) {
267
- return (value as any).value.map(Number);
266
+ if (value && typeof value === "object" && "value" in value && Array.isArray((value as { value: unknown }).value)) {
267
+ return (value as { value: unknown[] }).value.map(Number);
268
268
  }
269
269
  if (Array.isArray(value)) {
270
270
  return value.map(Number);
@@ -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: any = rolesSchema ? rolesSchema.table.bind(rolesSchema) : pgTable;
12
- const usersTableCreator: any = usersSchema ? usersSchema.table.bind(usersSchema) : 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
@@ -23,53 +22,19 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
23
22
  emailVerified: boolean("email_verified").default(false).notNull(),
24
23
  emailVerificationToken: varchar("email_verification_token", { length: 255 }),
25
24
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
26
- metadata: jsonb("metadata").$type<Record<string, any>>().default({}).notNull(),
25
+ isAnonymous: boolean("is_anonymous").default(false).notNull(),
26
+ roles: text("roles").array().default([]).notNull(),
27
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
27
28
  createdAt: timestamp("created_at").defaultNow().notNull(),
28
29
  updatedAt: timestamp("updated_at").defaultNow().notNull()
29
30
  });
30
31
 
31
- /**
32
- * Roles table - defines permission sets
33
- */
34
- const roles = rolesTableCreator("roles", {
35
- id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
36
- name: varchar("name", { length: 100 }).notNull(),
37
- isAdmin: boolean("is_admin").default(false).notNull(),
38
- defaultPermissions: jsonb("default_permissions").$type<{
39
- read?: boolean;
40
- create?: boolean;
41
- edit?: boolean;
42
- delete?: boolean;
43
- }>(),
44
- collectionPermissions: jsonb("collection_permissions").$type<
45
- Record<string, {
46
- read?: boolean;
47
- create?: boolean;
48
- edit?: boolean;
49
- delete?: boolean;
50
- }>
51
- >(),
52
- config: jsonb("config").$type<{
53
- createCollections?: boolean;
54
- editCollections?: "own" | "all" | boolean;
55
- deleteCollections?: "own" | "all" | boolean;
56
- }>()
57
- });
58
32
 
59
- /**
60
- * User-Role junction table
61
- */
62
- const userRoles = rolesTableCreator("user_roles", {
63
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
64
- roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
65
- }, (table: any) => ({
66
- pk: primaryKey({ columns: [table.userId, table.roleId] })
67
- }));
68
33
 
69
34
  /**
70
35
  * Refresh tokens for long-lived sessions
71
36
  */
72
- const refreshTokens = rolesTableCreator("refresh_tokens", {
37
+ const refreshTokens = tableCreator("refresh_tokens", {
73
38
  id: uuid("id").defaultRandom().primaryKey(),
74
39
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
75
40
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
@@ -77,14 +42,14 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
77
42
  userAgent: varchar("user_agent", { length: 500 }),
78
43
  ipAddress: varchar("ip_address", { length: 45 }),
79
44
  createdAt: timestamp("created_at").defaultNow().notNull()
80
- }, (table: any) => ({
45
+ }, (table) => ({
81
46
  uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
82
47
  }));
83
48
 
84
49
  /**
85
50
  * Password reset tokens for forgot password flow
86
51
  */
87
- const passwordResetTokens = rolesTableCreator("password_reset_tokens", {
52
+ const passwordResetTokens = tableCreator("password_reset_tokens", {
88
53
  id: uuid("id").defaultRandom().primaryKey(),
89
54
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
90
55
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
@@ -96,7 +61,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
96
61
  /**
97
62
  * App config - key/value store for custom settings
98
63
  */
99
- const appConfig = rolesTableCreator("app_config", {
64
+ const appConfig = tableCreator("app_config", {
100
65
  key: varchar("key", { length: 100 }).primaryKey(),
101
66
  value: jsonb("value").notNull(),
102
67
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -105,7 +70,7 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
105
70
  /**
106
71
  * User identities - maps external OAuth profiles back to local users
107
72
  */
108
- const userIdentities = rolesTableCreator("user_identities", {
73
+ const userIdentities = tableCreator("user_identities", {
109
74
  id: uuid("id").defaultRandom().primaryKey(),
110
75
  userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
111
76
  provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
@@ -113,77 +78,122 @@ export function createAuthSchema(rolesSchemaName: string = "rebase", usersSchema
113
78
  profileData: jsonb("profile_data"),
114
79
  createdAt: timestamp("created_at").defaultNow().notNull(),
115
80
  updatedAt: timestamp("updated_at").defaultNow().notNull()
116
- }, (table: any) => ({
81
+ }, (table) => ({
117
82
  uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
118
83
  }));
119
84
 
85
+ /**
86
+ * MFA factors table - stores enrolled MFA methods
87
+ */
88
+ const mfaFactors = tableCreator("mfa_factors", {
89
+ id: uuid("id").defaultRandom().primaryKey(),
90
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
91
+ factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
92
+ secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
93
+ friendlyName: varchar("friendly_name", { length: 255 }),
94
+ verified: boolean("verified").default(false).notNull(),
95
+ createdAt: timestamp("created_at").defaultNow().notNull(),
96
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
97
+ });
98
+
99
+ /**
100
+ * MFA challenges table - tracks active MFA verification attempts
101
+ */
102
+ const mfaChallenges = tableCreator("mfa_challenges", {
103
+ id: uuid("id").defaultRandom().primaryKey(),
104
+ factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
105
+ createdAt: timestamp("created_at").defaultNow().notNull(),
106
+ verifiedAt: timestamp("verified_at"),
107
+ ipAddress: varchar("ip_address", { length: 45 }),
108
+ expiresAt: timestamp("expires_at").notNull()
109
+ });
110
+
111
+ /**
112
+ * Recovery codes table - backup codes for MFA
113
+ */
114
+ const recoveryCodes = tableCreator("recovery_codes", {
115
+ id: uuid("id").defaultRandom().primaryKey(),
116
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
117
+ codeHash: varchar("code_hash", { length: 255 }).notNull(),
118
+ usedAt: timestamp("used_at"),
119
+ createdAt: timestamp("created_at").defaultNow().notNull()
120
+ });
121
+
120
122
  return {
121
- rolesSchema,
122
123
  usersSchema,
123
124
  users,
124
- roles,
125
- userRoles,
126
125
  refreshTokens,
127
126
  passwordResetTokens,
128
127
  appConfig,
129
- userIdentities
128
+ userIdentities,
129
+ mfaFactors,
130
+ mfaChallenges,
131
+ recoveryCodes
130
132
  };
131
133
  }
132
134
 
133
135
  // Instantiate default schema and tables using the default "rebase" schema
134
- const defaultAuthSchema = createAuthSchema("rebase", "rebase");
136
+ const defaultAuthSchema = createAuthSchema("rebase");
135
137
 
136
- export const rebaseSchema = defaultAuthSchema.rolesSchema;
137
138
  export const usersSchema = defaultAuthSchema.usersSchema;
138
139
 
139
140
  export const users = defaultAuthSchema.users;
140
- export const roles = defaultAuthSchema.roles;
141
- export const userRoles = defaultAuthSchema.userRoles;
142
141
  export const refreshTokens = defaultAuthSchema.refreshTokens;
143
142
  export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
144
143
  export const appConfig = defaultAuthSchema.appConfig;
145
144
  export const userIdentities = defaultAuthSchema.userIdentities;
145
+ export const mfaFactors = defaultAuthSchema.mfaFactors;
146
+ export const mfaChallenges = defaultAuthSchema.mfaChallenges;
147
+ export const recoveryCodes = defaultAuthSchema.recoveryCodes;
146
148
 
147
149
  // Relations
148
150
  export const usersRelations = relations(users, ({ many }) => ({
149
- userRoles: many(userRoles),
150
151
  refreshTokens: many(refreshTokens),
151
152
  passwordResetTokens: many(passwordResetTokens),
152
- userIdentities: many(userIdentities)
153
+ userIdentities: many(userIdentities),
154
+ mfaFactors: many(mfaFactors),
155
+ recoveryCodes: many(recoveryCodes)
153
156
  }));
154
157
 
155
- export const rolesRelations = relations(roles, ({ many }) => ({
156
- userRoles: many(userRoles)
158
+ export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
159
+ user: one(users, {
160
+ fields: [refreshTokens.userId],
161
+ references: [users.id]
162
+ })
157
163
  }));
158
164
 
159
- export const userRolesRelations = relations(userRoles, ({ one }) => ({
165
+ export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
160
166
  user: one(users, {
161
- fields: [userRoles.userId],
167
+ fields: [passwordResetTokens.userId],
162
168
  references: [users.id]
163
- }),
164
- role: one(roles, {
165
- fields: [userRoles.roleId],
166
- references: [roles.id]
167
169
  })
168
170
  }));
169
171
 
170
- export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
172
+ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
171
173
  user: one(users, {
172
- fields: [refreshTokens.userId],
174
+ fields: [userIdentities.userId],
173
175
  references: [users.id]
174
176
  })
175
177
  }));
176
178
 
177
- export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
179
+ export const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({
178
180
  user: one(users, {
179
- fields: [passwordResetTokens.userId],
181
+ fields: [mfaFactors.userId],
180
182
  references: [users.id]
183
+ }),
184
+ challenges: many(mfaChallenges)
185
+ }));
186
+
187
+ export const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({
188
+ factor: one(mfaFactors, {
189
+ fields: [mfaChallenges.factorId],
190
+ references: [mfaFactors.id]
181
191
  })
182
192
  }));
183
193
 
184
- export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
194
+ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
185
195
  user: one(users, {
186
- fields: [userIdentities.userId],
196
+ fields: [recoveryCodes.userId],
187
197
  references: [users.id]
188
198
  })
189
199
  }));
@@ -191,11 +201,11 @@ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
191
201
  // Type exports
192
202
  export type User = typeof users.$inferSelect;
193
203
  export type NewUser = typeof users.$inferInsert;
194
- export type Role = typeof roles.$inferSelect;
195
- export type NewRole = typeof roles.$inferInsert;
196
- export type UserRole = typeof userRoles.$inferSelect;
197
204
  export type RefreshToken = typeof refreshTokens.$inferSelect;
198
205
  export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
199
206
  export type AppConfig = typeof appConfig.$inferSelect;
200
207
  export type UserIdentity = typeof userIdentities.$inferSelect;
201
208
  export type NewUserIdentity = typeof userIdentities.$inferInsert;
209
+ export type MfaFactorRow = typeof mfaFactors.$inferSelect;
210
+ export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
211
+ export type RecoveryCodeRow = typeof recoveryCodes.$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 "./default-collections";
8
+ import { defaultUsersCollection } from "@rebasepro/common";
9
9
 
10
10
  // --- Helper Functions ---
11
11
 
@@ -90,11 +90,11 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
90
90
  collections = [];
91
91
  }
92
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);
97
- }
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
+ );
98
98
 
99
99
  // Sort collections by slug alphabetically to ensure deterministic schema generation
100
100
  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: {")) {