@rebasepro/server-postgresql 0.3.0 → 0.4.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 (49) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +5 -8
  2. package/dist/common/src/data/query_builder.d.ts +6 -2
  3. package/dist/index.es.js +301 -500
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +297 -496
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
  8. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
  9. package/dist/server-postgresql/src/auth/services.d.ts +6 -31
  10. package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
  11. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
  12. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
  13. package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
  14. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
  15. package/dist/types/src/controllers/auth.d.ts +2 -2
  16. package/dist/types/src/controllers/client.d.ts +25 -40
  17. package/dist/types/src/controllers/data.d.ts +21 -3
  18. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  19. package/dist/types/src/controllers/email.d.ts +2 -0
  20. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  21. package/dist/types/src/types/backend.d.ts +2 -2
  22. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  23. package/dist/types/src/types/collections.d.ts +9 -5
  24. package/dist/types/src/types/entity_views.d.ts +19 -28
  25. package/dist/types/src/types/properties.d.ts +9 -7
  26. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  27. package/dist/types/src/users/index.d.ts +0 -1
  28. package/dist/types/src/users/user.d.ts +0 -1
  29. package/package.json +6 -6
  30. package/src/PostgresBackendDriver.ts +10 -0
  31. package/src/PostgresBootstrapper.ts +25 -21
  32. package/src/auth/ensure-tables.ts +82 -129
  33. package/src/auth/services.ts +71 -170
  34. package/src/schema/auth-schema.ts +13 -69
  35. package/src/schema/doctor.ts +44 -3
  36. package/src/schema/generate-drizzle-schema-logic.ts +33 -3
  37. package/src/schema/generate-drizzle-schema.ts +2 -6
  38. package/src/schema/introspect-db-logic.ts +7 -0
  39. package/src/services/EntityFetchService.ts +13 -1
  40. package/src/services/EntityPersistService.ts +9 -0
  41. package/src/services/entityService.ts +7 -0
  42. package/src/utils/drizzle-conditions.ts +40 -5
  43. package/src/websocket.ts +1 -3
  44. package/test/auth-services.test.ts +7 -150
  45. package/test/doctor.test.ts +6 -2
  46. package/test/relation-pipeline-gaps.test.ts +315 -0
  47. package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
  48. package/dist/types/src/users/roles.d.ts +0 -14
  49. package/src/schema/default-collections.ts +0 -69
@@ -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);
@@ -53,6 +53,15 @@ export class EntityPersistService {
53
53
  .where(eq(idField, parsedId));
54
54
  }
55
55
 
56
+ /**
57
+ * Delete all entities from a collection
58
+ */
59
+ async deleteAll(collectionPath: string, _databaseId?: string): Promise<void> {
60
+ const collection = getCollectionByPath(collectionPath, this.registry);
61
+ const table = getTableForCollection(collection, this.registry);
62
+ await this.db.delete(table);
63
+ }
64
+
56
65
  /**
57
66
  * Save an entity (create or update)
58
67
  */
@@ -169,6 +169,13 @@ export class EntityService implements EntityRepository {
169
169
  return this.persistService.deleteEntity(collectionPath, entityId, databaseId);
170
170
  }
171
171
 
172
+ /**
173
+ * Delete all entities from a collection
174
+ */
175
+ async deleteAll(collectionPath: string, databaseId?: string): Promise<void> {
176
+ return this.persistService.deleteAll(collectionPath, databaseId);
177
+ }
178
+
172
179
 
173
180
  /**
174
181
  * Execute raw SQL
@@ -1,6 +1,6 @@
1
1
  import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
3
- import { FilterValues, WhereFilterOp, Relation, JoinStep } from "@rebasepro/types";
3
+ import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, FilterCondition } from "@rebasepro/types";
4
4
  import { getColumnName, resolveCollectionRelations } from "@rebasepro/common";
5
5
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
6
  import { ConditionBuilderStatic } from "../interfaces";
@@ -37,7 +37,6 @@ export class DrizzleConditionBuilder {
37
37
  for (const [field, filterParam] of Object.entries(filter)) {
38
38
  if (!filterParam) continue;
39
39
 
40
- const [op, value] = filterParam as [WhereFilterOp, any];
41
40
  let fieldColumn = table[field as keyof typeof table] as AnyPgColumn;
42
41
 
43
42
  if (!fieldColumn) {
@@ -53,15 +52,51 @@ export class DrizzleConditionBuilder {
53
52
  continue;
54
53
  }
55
54
 
56
- const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
57
- if (condition) {
58
- conditions.push(condition);
55
+ const paramsList = Array.isArray(filterParam) && filterParam.length > 0 && Array.isArray(filterParam[0])
56
+ ? (filterParam as [WhereFilterOp, any][])
57
+ : [filterParam as [WhereFilterOp, any]];
58
+
59
+ for (const [op, value] of paramsList) {
60
+ const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
61
+ if (condition) {
62
+ conditions.push(condition);
63
+ }
59
64
  }
60
65
  }
61
66
 
62
67
  return conditions;
63
68
  }
64
69
 
70
+ /**
71
+ * Build logical conditions recursively from LogicalCondition or FilterCondition
72
+ */
73
+ static buildLogicalConditions(
74
+ cond: LogicalCondition | FilterCondition,
75
+ table: PgTable<any>,
76
+ collectionPath: string
77
+ ): SQL | null {
78
+ if ("type" in cond) {
79
+ const subSQLs = cond.conditions
80
+ .map(c => this.buildLogicalConditions(c, table, collectionPath))
81
+ .filter((sql): sql is SQL => sql !== null);
82
+ if (subSQLs.length === 0) return null;
83
+ return (cond.type === "or" ? or(...subSQLs) : and(...subSQLs)) ?? null;
84
+ } else {
85
+ let fieldColumn = table[cond.column as keyof typeof table] as AnyPgColumn;
86
+ if (!fieldColumn) {
87
+ const relationKey = `${cond.column}_id`;
88
+ if (relationKey in table) {
89
+ fieldColumn = table[relationKey as keyof typeof table] as AnyPgColumn;
90
+ }
91
+ }
92
+ if (!fieldColumn) {
93
+ console.warn(`Filtering by field '${cond.column}', but it does not exist in table for collection '${collectionPath}'`);
94
+ return null;
95
+ }
96
+ return this.buildSingleFilterCondition(fieldColumn, cond.operator as WhereFilterOp, cond.value);
97
+ }
98
+ }
99
+
65
100
  /**
66
101
  * Build a single filter condition for a specific operator and value
67
102
  */
package/src/websocket.ts CHANGED
@@ -547,15 +547,13 @@ colors: true }));
547
547
  }
548
548
  break;
549
549
 
550
- // Route subscription messages to RealtimeService
550
+ // Route subscription messages, broadcast channels, and presence to RealtimeService
551
551
  case "subscribe_collection":
552
552
  case "subscribe_entity":
553
553
  case "unsubscribe":
554
- // Broadcast channels
555
554
  case "join_channel":
556
555
  case "leave_channel":
557
556
  case "broadcast":
558
- // Presence
559
557
  case "presence_track":
560
558
  case "presence_untrack":
561
559
  case "presence_state": {
@@ -1,5 +1,5 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { UserService, RoleService, RefreshTokenService, PasswordResetTokenService, Role } from "../src/auth/services";
2
+ import { UserService, RefreshTokenService, PasswordResetTokenService, Role } from "../src/auth/services";
3
3
  import { users, refreshTokens, passwordResetTokens } from "../src/schema/auth-schema";
4
4
  import { UserData } from "@rebasepro/server-core";
5
5
 
@@ -344,18 +344,7 @@ describe("Auth Services", () => {
344
344
  describe("getUserRoles", () => {
345
345
  it("should return roles for user", async () => {
346
346
  mockExecute.mockResolvedValueOnce({
347
- rows: [
348
- { id: "admin",
349
- name: "Admin",
350
- is_admin: true,
351
- default_permissions: null,
352
- collection_permissions: null },
353
- { id: "editor",
354
- name: "Editor",
355
- is_admin: false,
356
- default_permissions: { edit: true },
357
- collection_permissions: null }
358
- ]
347
+ rows: [{ roles: ["admin", "editor"] }]
359
348
  });
360
349
 
361
350
  const roles = await userService.getUserRoles("user-123");
@@ -363,7 +352,7 @@ describe("Auth Services", () => {
363
352
  expect(roles).toHaveLength(2);
364
353
  expect(roles[0]).toEqual({
365
354
  id: "admin",
366
- name: "Admin",
355
+ name: "admin",
367
356
  isAdmin: true,
368
357
  defaultPermissions: null,
369
358
  collectionPermissions: null
@@ -374,13 +363,7 @@ describe("Auth Services", () => {
374
363
  describe("getUserRoleIds", () => {
375
364
  it("should return role IDs for user", async () => {
376
365
  mockExecute.mockResolvedValueOnce({
377
- rows: [
378
- { id: "admin",
379
- name: "Admin",
380
- is_admin: true,
381
- default_permissions: null,
382
- collection_permissions: null }
383
- ]
366
+ rows: [{ roles: ["admin"] }]
384
367
  });
385
368
 
386
369
  const roleIds = await userService.getUserRoleIds("user-123");
@@ -393,10 +376,7 @@ describe("Auth Services", () => {
393
376
  it("should delete existing and insert new roles", async () => {
394
377
  await userService.setUserRoles("user-123", ["admin", "editor"]);
395
378
 
396
- // First call deletes existing roles
397
379
  expect(mockExecute).toHaveBeenCalled();
398
- // Subsequent calls insert new roles
399
- expect(mockExecute.mock.calls.length).toBeGreaterThanOrEqual(1);
400
380
  });
401
381
  });
402
382
 
@@ -408,7 +388,7 @@ describe("Auth Services", () => {
408
388
  });
409
389
 
410
390
  it("should use editor as default role", async () => {
411
- await userService.assignDefaultRole("user-123");
391
+ await userService.assignDefaultRole("user-123", "editor");
412
392
 
413
393
  expect(mockExecute).toHaveBeenCalled();
414
394
  });
@@ -419,11 +399,7 @@ describe("Auth Services", () => {
419
399
  const mockUser = { id: "user-123", email: "test@example.com" };
420
400
  mockSelectWhere.mockResolvedValueOnce([mockUser]);
421
401
  mockExecute.mockResolvedValueOnce({
422
- rows: [{ id: "admin",
423
- name: "Admin",
424
- is_admin: true,
425
- default_permissions: null,
426
- collection_permissions: null }]
402
+ rows: [{ roles: ["admin"] }]
427
403
  });
428
404
 
429
405
  const result = await userService.getUserWithRoles("user-123");
@@ -431,7 +407,7 @@ describe("Auth Services", () => {
431
407
  expect(result).toEqual({
432
408
  user: mockUserData({}),
433
409
  roles: [{ id: "admin",
434
- name: "Admin",
410
+ name: "admin",
435
411
  isAdmin: true,
436
412
  defaultPermissions: null,
437
413
  collectionPermissions: null }]
@@ -472,125 +448,6 @@ describe("Auth Services", () => {
472
448
  });
473
449
  });
474
450
 
475
- describe("RoleService", () => {
476
- let roleService: RoleService;
477
-
478
- beforeEach(() => {
479
- roleService = new RoleService(db);
480
- });
481
-
482
- describe("getRoleById", () => {
483
- it("should return role when found", async () => {
484
- mockExecute.mockResolvedValueOnce({
485
- rows: [{ id: "admin",
486
- name: "Admin",
487
- is_admin: true,
488
- default_permissions: null,
489
- collection_permissions: null }]
490
- });
491
-
492
- const result = await roleService.getRoleById("admin");
493
-
494
- expect(result).toEqual({
495
- id: "admin",
496
- name: "Admin",
497
- isAdmin: true,
498
- defaultPermissions: null,
499
- collectionPermissions: null
500
- });
501
- });
502
-
503
- it("should return null when role not found", async () => {
504
- mockExecute.mockResolvedValueOnce({ rows: [] });
505
-
506
- const result = await roleService.getRoleById("nonexistent");
507
-
508
- expect(result).toBeNull();
509
- });
510
- });
511
-
512
- describe("listRoles", () => {
513
- it("should return all roles", async () => {
514
- mockExecute.mockResolvedValueOnce({
515
- rows: [
516
- { id: "admin",
517
- name: "Admin",
518
- is_admin: true,
519
- default_permissions: null,
520
- collection_permissions: null },
521
- { id: "editor",
522
- name: "Editor",
523
- is_admin: false,
524
- default_permissions: null,
525
- collection_permissions: null }
526
- ]
527
- });
528
-
529
- const roles = await roleService.listRoles();
530
-
531
- expect(roles).toHaveLength(2);
532
- });
533
- });
534
-
535
- describe("createRole", () => {
536
- it("should create a role", async () => {
537
- mockExecute.mockResolvedValueOnce({
538
- rows: [{ id: "custom",
539
- name: "Custom Role",
540
- is_admin: false,
541
- default_permissions: null,
542
- collection_permissions: null }]
543
- });
544
-
545
- const role = await roleService.createRole({
546
- id: "custom",
547
- name: "Custom Role",
548
- defaultPermissions: null
549
- });
550
-
551
- expect(role.id).toBe("custom");
552
- expect(role.name).toBe("Custom Role");
553
- });
554
- });
555
-
556
- describe("updateRole", () => {
557
- it("should update a role", async () => {
558
- mockExecute
559
- .mockResolvedValueOnce({ rows: [{ id: "admin",
560
- name: "Admin",
561
- is_admin: true,
562
- default_permissions: null,
563
- collection_permissions: null }] })
564
- .mockResolvedValueOnce({ rows: [] })
565
- .mockResolvedValueOnce({ rows: [{ id: "admin",
566
- name: "Super Admin",
567
- is_admin: true,
568
- default_permissions: null,
569
- collection_permissions: null }] });
570
-
571
- const result = await roleService.updateRole("admin", { name: "Super Admin" });
572
-
573
- expect(result?.name).toBe("Super Admin");
574
- });
575
-
576
- it("should return null when role not found", async () => {
577
- mockExecute.mockResolvedValueOnce({ rows: [] });
578
-
579
- const result = await roleService.updateRole("nonexistent", { name: "Test" });
580
-
581
- expect(result).toBeNull();
582
- });
583
- });
584
-
585
- describe("deleteRole", () => {
586
- it("should delete a role", async () => {
587
- await roleService.deleteRole("custom");
588
-
589
- expect(mockExecute).toHaveBeenCalled();
590
- });
591
- });
592
- });
593
-
594
451
  describe("RefreshTokenService", () => {
595
452
  let refreshTokenService: RefreshTokenService;
596
453
 
@@ -135,8 +135,12 @@ columnType: "time" } as DateProperty)).toBe("time without time zone");
135
135
  it("should map json types correctly", () => {
136
136
  expect(getExpectedColumnType({ type: "map" })).toBe("jsonb");
137
137
  expect(getExpectedColumnType({ type: "array" })).toBe("jsonb");
138
- expect(getExpectedColumnType({ type: "array",
139
- columnType: "json" } as ArrayProperty)).toBe("json");
138
+ expect(getExpectedColumnType({ type: "array", columnType: "json" } as ArrayProperty)).toBe("json");
139
+
140
+ // Native array element type mappings
141
+ expect(getExpectedColumnType({ type: "array", of: { type: "string" } } as ArrayProperty)).toBe("ARRAY");
142
+ expect(getExpectedColumnType({ type: "array", of: { type: "number", validation: { integer: true } } } as ArrayProperty)).toBe("ARRAY");
143
+ expect(getExpectedColumnType({ type: "array", of: { type: "boolean" } } as ArrayProperty)).toBe("ARRAY");
140
144
  });
141
145
 
142
146
  it("should map enum string to USER-DEFINED", () => {