@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.g009ed95

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 (98) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/PostgresBootstrapper.d.ts +33 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/backup/backup-service.d.ts +10 -1
  5. package/dist/backup/pg-tools.d.ts +47 -0
  6. package/dist/backup-service-CD8o_1Sl.js +8999 -0
  7. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  8. package/dist/cli-helpers.d.ts +39 -0
  9. package/dist/collections/buildRegistry.d.ts +1 -1
  10. package/dist/connection-BuZ97wsr.js +250 -0
  11. package/dist/connection-BuZ97wsr.js.map +1 -0
  12. package/dist/connection.d.ts +42 -0
  13. package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
  14. package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
  15. package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
  16. package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
  17. package/dist/history/HistoryService.d.ts +9 -29
  18. package/dist/index.es.js +1234 -9753
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/policy-CeA1JcxP.js +105 -0
  21. package/dist/policy-CeA1JcxP.js.map +1 -0
  22. package/dist/schema/auth-schema.d.ts +83 -144
  23. package/dist/schema/dynamic-tables.d.ts +1 -1
  24. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  25. package/dist/schema/ensure-collection-tables.d.ts +44 -2
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
  27. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  28. package/dist/schema/introspect-db-logic.d.ts +94 -5
  29. package/dist/schema/introspect-db-queries.d.ts +119 -0
  30. package/dist/schema/introspect-db-structure.d.ts +263 -0
  31. package/dist/schema/introspect-db-types.d.ts +11 -0
  32. package/dist/schema/introspect-runtime.d.ts +1 -1
  33. package/dist/services/FetchService.d.ts +40 -2
  34. package/dist/services/RelationService.d.ts +24 -1
  35. package/dist/services/channel-bus/index.d.ts +1 -7
  36. package/dist/services/collection-helpers.d.ts +24 -1
  37. package/dist/services/dataService.d.ts +3 -1
  38. package/dist/services/row-pipeline.d.ts +4 -2
  39. package/dist/{src-3VmUJ8Xn.js → src-CzbghKwf.js} +464 -187
  40. package/dist/src-CzbghKwf.js.map +1 -0
  41. package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
  42. package/dist/src-DoU9yPqq.js.map +1 -0
  43. package/dist/utils/connection-string.d.ts +29 -0
  44. package/dist/utils/drizzle-conditions.d.ts +162 -7
  45. package/dist/utils/pg-error-utils.d.ts +25 -3
  46. package/dist/websocket-B2LsrINK.js +530 -0
  47. package/dist/websocket-B2LsrINK.js.map +1 -0
  48. package/package.json +14 -14
  49. package/src/PostgresAdapter.ts +21 -2
  50. package/src/PostgresBackendDriver.ts +4 -0
  51. package/src/PostgresBootstrapper.ts +212 -36
  52. package/src/auth/ensure-tables.ts +164 -9
  53. package/src/auth/services.ts +24 -2
  54. package/src/backup/backup-cli.ts +41 -2
  55. package/src/backup/backup-service.ts +38 -5
  56. package/src/backup/pg-tools.ts +96 -3
  57. package/src/cli-helpers.ts +70 -0
  58. package/src/cli.ts +44 -26
  59. package/src/collections/buildRegistry.ts +1 -1
  60. package/src/collections/validate-relations.ts +15 -0
  61. package/src/connection.ts +73 -0
  62. package/src/data-transformer.ts +9 -3
  63. package/src/databasePoolManager.ts +5 -2
  64. package/src/history/HistoryService.ts +13 -31
  65. package/src/schema/auth-schema.ts +30 -19
  66. package/src/schema/dynamic-tables.ts +1 -1
  67. package/src/schema/ensure-collection-policies.ts +105 -0
  68. package/src/schema/ensure-collection-tables.test.ts +105 -9
  69. package/src/schema/ensure-collection-tables.ts +220 -32
  70. package/src/schema/generate-drizzle-schema-logic.ts +33 -8
  71. package/src/schema/generate-postgres-ddl-logic.ts +382 -19
  72. package/src/schema/introspect-db-constraints.ts +385 -0
  73. package/src/schema/introspect-db-inference.ts +18 -8
  74. package/src/schema/introspect-db-logic.ts +385 -71
  75. package/src/schema/introspect-db-queries.ts +326 -0
  76. package/src/schema/introspect-db-structure.ts +670 -0
  77. package/src/schema/introspect-db-types.ts +56 -0
  78. package/src/schema/introspect-db.ts +37 -80
  79. package/src/schema/introspect-runtime.test.ts +56 -8
  80. package/src/schema/introspect-runtime.ts +32 -10
  81. package/src/security/policy-drift.test.ts +11 -3
  82. package/src/services/FetchService.ts +148 -18
  83. package/src/services/PersistService.ts +20 -6
  84. package/src/services/RelationService.ts +249 -48
  85. package/src/services/channel-bus/index.ts +0 -9
  86. package/src/services/collection-helpers.ts +40 -1
  87. package/src/services/dataService.ts +3 -1
  88. package/src/services/realtimeService.ts +3 -3
  89. package/src/services/row-pipeline.ts +4 -2
  90. package/src/utils/connection-string.ts +58 -0
  91. package/src/utils/drizzle-conditions.ts +539 -50
  92. package/src/utils/pg-error-utils.ts +98 -3
  93. package/src/websocket.ts +18 -9
  94. package/dist/chunk-DSJWtz9O.js +0 -40
  95. package/dist/ensure-collection-tables-DGMYK0fr.js +0 -304
  96. package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
  97. package/dist/src-3VmUJ8Xn.js.map +0 -1
  98. package/dist/src-D5xBTl32.js.map +0 -1
@@ -1,12 +1,14 @@
1
- import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
1
+ import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  import { CollectionConfig, FilterValues, ResolvedRelation, LogicalCondition, isManyToMany } from "@rebasepro/types";
4
4
  import type { VectorSearchParams } from "@rebasepro/types";
5
5
  import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
6
- import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
6
+ import { generateForeignKeyName } from "@rebasepro/utils";
7
+ import { DrizzleConditionBuilder, getUnknownFilterFieldsMode, type FilterCompilationOptions } from "../utils/drizzle-conditions";
7
8
  import {
8
9
  getCollectionByPath,
9
10
  getTableForCollection,
11
+ getPrimaryKeys,
10
12
  requirePrimaryKeys,
11
13
  deriveRowAddress,
12
14
  parseIdValues,
@@ -18,9 +20,9 @@ import { RelationService } from "./RelationService";
18
20
  import { RelationalQueryBuilder } from "drizzle-orm/pg-core/query-builders/query";
19
21
  import { DrizzleClient } from "../interfaces";
20
22
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
21
- import { toCmsRow, toRestRow, isJunctionRelation } from "./row-pipeline";
23
+ import { toFlatRow, toRestRow, isJunctionRelation } from "./row-pipeline";
22
24
  import { isNestedPath, resolveNestedPath, type NestedPathHop } from "./nested-path";
23
- import { logger } from "@rebasepro/server";
25
+ import { ApiError, logger } from "@rebasepro/server";
24
26
 
25
27
  /** Type-safe accessor for Drizzle's relational query API via dynamic table name */
26
28
  type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
@@ -45,6 +47,45 @@ export class FetchService {
45
47
  return query?.[tableName] as RelationalQueryBuilder<TablesRelationalConfig, TableRelationalConfig> | undefined;
46
48
  }
47
49
 
50
+ /**
51
+ * The context the condition builder needs to compile a filter key that is
52
+ * not a column name outright.
53
+ *
54
+ * Two such keys. An owning relation's key resolves through the collection's
55
+ * relations to its foreign-key column; a relation whose link lives on the
56
+ * target table or in a junction resolves to a correlated `EXISTS`, which
57
+ * needs the registry to reach that other table and this table's key column
58
+ * to correlate back.
59
+ *
60
+ * Looked up rather than passed: every read path already has the path, only
61
+ * some have the collection, and a path that names no registered collection
62
+ * (a nested/derived one) is not an error here — the builder simply falls
63
+ * back to guessing the default key shapes, and a relation filter it cannot
64
+ * compile stays unresolvable and so fails closed.
65
+ */
66
+ private filterContext(collectionPath: string, table: PgTable<any>): FilterCompilationOptions {
67
+ const collection = this.registry.getCollectionByPath(collectionPath) ?? undefined;
68
+ return {
69
+ collection,
70
+ registry: this.registry,
71
+ sourceIdColumn: collection ? this.resolveIdColumn(collection, table) : undefined
72
+ };
73
+ }
74
+
75
+ /**
76
+ * The table column this collection's rows are keyed by, or `undefined`.
77
+ *
78
+ * `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
79
+ * resolvable key is not an error on the filter path — it only means the
80
+ * relation filters that would correlate on it cannot be compiled, which
81
+ * the builder already handles by failing that field closed.
82
+ */
83
+ private resolveIdColumn(collection: CollectionConfig, table: PgTable<any>): AnyPgColumn | undefined {
84
+ const [idInfo] = getPrimaryKeys(collection, this.registry);
85
+ if (!idInfo) return undefined;
86
+ return table[idInfo.fieldName as keyof typeof table] as AnyPgColumn | undefined;
87
+ }
88
+
48
89
  /**
49
90
  * Build filter conditions from FilterValues
50
91
  * Delegates to DrizzleConditionBuilder.buildFilterConditions
@@ -54,7 +95,9 @@ export class FetchService {
54
95
  table: PgTable<any>,
55
96
  collectionPath: string
56
97
  ): SQL[] {
57
- return DrizzleConditionBuilder.buildFilterConditions(filter, table, collectionPath);
98
+ return DrizzleConditionBuilder.buildFilterConditions(
99
+ filter, table, collectionPath, this.filterContext(collectionPath, table)
100
+ );
58
101
  }
59
102
 
60
103
  // =============================================================
@@ -64,20 +107,97 @@ export class FetchService {
64
107
  /**
65
108
  * Resolves the correct Drizzle column for sorting.
66
109
  * Automatically maps owning relation property keys to their underlying foreign key column.
110
+ *
111
+ * The relation's own `localKey` is the authority for that foreign key, not
112
+ * `<field>_id`. The default local key comes from `generateForeignKeyName`,
113
+ * which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
114
+ * `users` → `user_id` — and an author can override it outright. A wrong
115
+ * guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
116
+ * come back in whatever order Postgres pleases: paging over that repeats
117
+ * and skips rows rather than erroring. The guesses stay, last, for a
118
+ * caller that hands over no collection to resolve against.
67
119
  */
68
120
  private resolveOrderByField(
69
121
  table: PgTable<any>,
70
122
  orderBy: string,
71
123
  collection?: CollectionConfig
72
124
  ): AnyPgColumn | undefined {
73
- let orderByField = table[orderBy as keyof typeof table] as AnyPgColumn;
74
- if (!orderByField && collection) {
75
- const property = collection.properties[orderBy];
76
- if (property && property.type === "relation" && "relation" in property && property.resolvedRelation?.kind === "belongsTo") {
77
- orderByField = table[`${orderBy}_id` as keyof typeof table] as AnyPgColumn;
78
- }
125
+ const columnAt = (key: string): AnyPgColumn | undefined =>
126
+ (key in table ? table[key as keyof typeof table] as AnyPgColumn : undefined) || undefined;
127
+
128
+ const direct = columnAt(orderBy);
129
+ if (direct) return direct;
130
+
131
+ // Owning relation, resolved: the relation names its own local key.
132
+ const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : undefined;
133
+ if (declaredRelation?.kind === "belongsTo") {
134
+ const foreignKey = columnAt(declaredRelation.localKey);
135
+ if (foreignKey) return foreignKey;
136
+ }
137
+
138
+ // No collection in hand — the two shapes an owning relation's key takes
139
+ // by default (e.g. `project` → `project_id`, `userProfile` →
140
+ // `user_profile_id`).
141
+ for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
142
+ const foreignKey = columnAt(guess);
143
+ if (foreignKey) return foreignKey;
79
144
  }
80
- return orderByField;
145
+
146
+ // Nothing resolved. Returning `undefined` is exactly what the docblock
147
+ // above describes: the caller drops the ORDER BY and hands back rows in
148
+ // whatever order Postgres pleases, while the requester believes they
149
+ // are sorted. `?orderBy=titel` answered 200 with unsorted data and no
150
+ // hint that the sort had been ignored.
151
+ //
152
+ // A *filter* naming a field that does not exist is already refused for
153
+ // precisely this reason — it "used to widen results silently". An
154
+ // unresolvable sort field is the same drift between a query and the
155
+ // schema, so it answers the same way and honours the same switch: one
156
+ // knob, because a deployment that wants the lenient behaviour wants it
157
+ // for both.
158
+ const collectionName = collection?.slug ?? collection?.name;
159
+ const onCollection = collectionName ? ` on collection '${collectionName}'` : "";
160
+
161
+ // A declared to-many relation is a different mistake from a typo, and
162
+ // saying "unknown field" about a field the collection plainly declares
163
+ // sends the reader looking for a spelling error that is not there.
164
+ // There is simply no single value per row to order by — `posts.tags` is
165
+ // a set — so no ORDER BY exists to write, with or without a typo.
166
+ if (declaredRelation && declaredRelation.kind !== "belongsTo") {
167
+ throw ApiError.badRequest(
168
+ `Cannot sort by '${orderBy}'${onCollection}: it is a to-many relation ` +
169
+ `(${declaredRelation.kind}), which has no single value per row to order by.`,
170
+ "ORDER_BY_FIELD_NOT_SORTABLE",
171
+ { field: orderBy, kind: declaredRelation.kind, ...(collectionName && { collection: collectionName }) }
172
+ );
173
+ }
174
+
175
+ if (getUnknownFilterFieldsMode() === "warn") {
176
+ logger.warn(
177
+ `Sorting by field '${orderBy}'${onCollection}, but it does not exist in the table — ` +
178
+ "the ORDER BY was dropped and these rows are unsorted."
179
+ );
180
+ return undefined;
181
+ }
182
+
183
+ let validFields: string[] = [];
184
+ try {
185
+ validFields = Object.keys(getTableColumns(table)).sort();
186
+ } catch {
187
+ // A table stand-in without Drizzle's column symbols — the message
188
+ // is worth less without the list, but not worth failing over.
189
+ }
190
+
191
+ throw ApiError.badRequest(
192
+ `Unknown orderBy field '${orderBy}'${onCollection}` +
193
+ (validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""),
194
+ "UNKNOWN_ORDER_BY_FIELD",
195
+ {
196
+ field: orderBy,
197
+ ...(collectionName && { collection: collectionName }),
198
+ ...(validFields.length > 0 && { validFields })
199
+ }
200
+ );
81
201
  }
82
202
 
83
203
  /**
@@ -85,7 +205,7 @@ export class FetchService {
85
205
  * Converts collection relations to a Drizzle-compatible `with` object.
86
206
  *
87
207
  * When `include` is provided, only those relations are loaded.
88
- * When `include` is absent, ALL relations are loaded (CMS path).
208
+ * When `include` is absent, ALL relations are loaded (the admin path).
89
209
  *
90
210
  * Automatically detects many-to-many junction tables and nests
91
211
  * the target relation so actual row data is returned.
@@ -311,7 +431,7 @@ export class FetchService {
311
431
  }
312
432
 
313
433
  if (options.logical) {
314
- const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
434
+ const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
315
435
  if (logicalCondition) allConditions.push(logicalCondition);
316
436
  }
317
437
 
@@ -487,7 +607,7 @@ idColumn };
487
607
 
488
608
  if (!row) return undefined;
489
609
 
490
- const flatRow = toCmsRow(row, collection, this.registry);
610
+ const flatRow = toFlatRow(row, collection, this.registry);
491
611
 
492
612
  // Post-fetch joinPath relations that Drizzle's `with` can't express
493
613
  await this.resolveJoinPathRelations<M>(flatRow, collection, collectionPath, parsedId, databaseId);
@@ -614,7 +734,7 @@ idColumn };
614
734
  const results = await qb.findMany(queryOpts as Parameters<NonNullable<typeof qb>["findMany"]>[0]);
615
735
 
616
736
  const rows = (results as Record<string, unknown>[]).map(row =>
617
- toCmsRow(row, collection, this.registry)
737
+ toFlatRow(row, collection, this.registry)
618
738
  );
619
739
 
620
740
  return rows;
@@ -656,7 +776,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
656
776
  }
657
777
 
658
778
  if (options.logical) {
659
- const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
779
+ const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
660
780
  if (logicalCondition) allConditions.push(logicalCondition);
661
781
  }
662
782
 
@@ -717,7 +837,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
717
837
  /**
718
838
  * Fallback path used when db.query is unavailable.
719
839
  *
720
- * The primary path runs the results through `toCmsRow`, which maps
840
+ * The primary path runs the results through `toFlatRow`, which maps
721
841
  * relations from what drizzle already nested — no query per row. This one
722
842
  * has no nesting to read, so it resolves relations itself, in batches.
723
843
  *
@@ -874,6 +994,7 @@ relatedTo: hop });
874
994
  collectionPath: string,
875
995
  options: {
876
996
  filter?: FilterValues<Extract<keyof M, string>>;
997
+ logical?: LogicalCondition;
877
998
  searchString?: string;
878
999
  databaseId?: string;
879
1000
  } = {}
@@ -905,6 +1026,13 @@ relatedTo: hop });
905
1026
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
906
1027
  }
907
1028
 
1029
+ if (options.logical) {
1030
+ const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(
1031
+ options.logical, table, effectivePath, this.filterContext(effectivePath, table)
1032
+ );
1033
+ if (logicalCondition) allConditions.push(logicalCondition);
1034
+ }
1035
+
908
1036
  if (allConditions.length > 0) {
909
1037
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
910
1038
  if (finalCondition) query = query.where(finalCondition);
@@ -975,6 +1103,8 @@ relatedTo: hop });
975
1103
  collectionPath: string,
976
1104
  options: {
977
1105
  filter?: FilterValues<Extract<keyof M, string>>;
1106
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
1107
+ logical?: LogicalCondition;
978
1108
  orderBy?: string;
979
1109
  order?: "desc" | "asc";
980
1110
  limit?: number;
@@ -1,7 +1,7 @@
1
1
  import { eq, and, sql, SQL } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
3
  // import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
- import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany } from "@rebasepro/types";
4
+ import { CollectionConfig, Properties, ResolvedRelation, type ResolvedManyToMany, isManyToMany, hasForeignKeyOnTarget } from "@rebasepro/types";
5
5
  import { getTableName, resolveCollectionRelations } from "@rebasepro/common";
6
6
  import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
7
7
  import {
@@ -240,16 +240,30 @@ export class PersistService {
240
240
  throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to update.`);
241
241
  }
242
242
  } else {
243
- // One-to-many create: stamp the parent's id onto the child's FK.
243
+ // One-to-many create: stamp the parent's key onto the child's FK.
244
244
  const targetColumnName = this.resolveParentForeignKeyColumn(hop);
245
245
 
246
246
  if (targetColumnName) {
247
- const parsedParentId = parentIdForWrite();
247
+ // Not necessarily the id in the path: a link with a
248
+ // `sourceKey` is pointed at a column on the parent row, and
249
+ // stamping the id would create a child that belongs to
250
+ // nobody — a row the read path would never return again.
251
+ const parentKeyValue = hasForeignKeyOnTarget(hop.relation)
252
+ ? await this.relationService.parentKeyValue(hop.parentCollection, hop.relation, hop.parentId)
253
+ : parentIdForWrite();
254
+
255
+ if (parentKeyValue === undefined) {
256
+ throw ApiError.badRequest(
257
+ `Cannot create under "${collectionPath}": the parent row has no value in ` +
258
+ `\`sourceKey\`, so the new row has nothing to point at.`
259
+ );
260
+ }
261
+
248
262
  const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
249
- if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
250
- logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
263
+ if (existingValue !== undefined && existingValue !== null && existingValue !== parentKeyValue) {
264
+ logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent key '${parentKeyValue}'.`);
251
265
  }
252
- (effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
266
+ (effectiveValues as Record<string, unknown>)[targetColumnName] = parentKeyValue;
253
267
  }
254
268
  }
255
269
  }