@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g3660bd5

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.
package/dist/index.es.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
5
- import { A as toSnakeCase, C as normalizeToEntityRelation, D as getPolicyNamesForRule, F as Vector, N as hasForeignKeyOnTarget, O as mergeDeep, P as isManyToMany, S as createRelationRefWithData, T as generateForeignKeyName, _ as buildCompositeId, a as getJunctionSecurityRules, b as parseIdValues, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, k as camelCase, l as securityRuleToConditions, m as getTableName$1, n as CollectionRegistry, o as resolveJunctionSpecs, p as getEnumVarName, r as resolveStringColumnLength, s as getEffectiveSecurityRules, t as buildSdkData, u as findAnonymousGrants, v as getDeclaredPrimaryKeys, w as updateDateAutoValues, x as createRelationRef, y as isAddressableId } from "./src-DlPBctw_.js";
5
+ import { A as camelCase, C as normalizeToEntityRelation, D as getPolicyNamesForRule, E as legacyForeignKeyName, F as hasForeignKeyOnTarget, I as isManyToMany, L as Vector, O as isPrototypePollutingKey, P as resolveClientListLimit, S as createRelationRefWithData, T as generateForeignKeyName, _ as buildCompositeId, a as getJunctionSecurityRules, b as parseIdValues, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, j as toSnakeCase, k as mergeDeep, l as securityRuleToConditions, m as getTableName$1, n as CollectionRegistry, o as resolveJunctionSpecs, p as getEnumVarName, r as resolveStringColumnLength, s as getEffectiveSecurityRules, t as buildSdkData, u as findAnonymousGrants, v as getDeclaredPrimaryKeys, w as updateDateAutoValues, x as createRelationRef, y as isAddressableId } from "./src-Bs2ZzSi4.js";
6
6
  import { n as isPostgresCollectionConfig, r as isRelationalCollectionConfig } from "./src-DoU9yPqq.js";
7
7
  import { t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
8
8
  import { t as createPostgresWebSocket } from "./websocket-B2LsrINK.js";
@@ -30,25 +30,6 @@ import { randomUUID } from "crypto";
30
30
  function isChannelBusInstance(setting) {
31
31
  return typeof setting?.publish === "function";
32
32
  }
33
- /**
34
- * Resolve a client-supplied list `limit` into a safe, always-defined value.
35
- *
36
- * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
37
- * so `0`, negatives, and absurd values can never bypass the cap.
38
- * - An absent / blank / non-numeric limit falls back to the mode default:
39
- * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
40
- *
41
- * The return is never `undefined` — no ingress that routes its client limit
42
- * through this can produce an unbounded read.
43
- */
44
- function resolveClientListLimit(rawLimit, opts = {}) {
45
- const maxLimit = opts.maxLimit ?? 1e3;
46
- if (rawLimit != null && String(rawLimit).trim() !== "") {
47
- const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
48
- if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
49
- }
50
- return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
51
- }
52
33
  //#endregion
53
34
  //#region ../common/src/util/email.ts
54
35
  /**
@@ -1276,7 +1257,11 @@ function sanitizeAndConvertDates(obj) {
1276
1257
  if (obj instanceof Date) return obj.toISOString();
1277
1258
  if (typeof obj === "object") {
1278
1259
  const newObj = {};
1279
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = sanitizeAndConvertDates(obj[key]);
1260
+ for (const key in obj) {
1261
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
1262
+ if (isPrototypePollutingKey(key)) continue;
1263
+ newObj[key] = sanitizeAndConvertDates(obj[key]);
1264
+ }
1280
1265
  return newObj;
1281
1266
  }
1282
1267
  if (typeof obj === "string") {
@@ -1305,6 +1290,7 @@ function serializeDataToServer(row, properties, collection, registry) {
1305
1290
  if (relation.kind === "belongsTo") foreignKeys.add(relation.localKey);
1306
1291
  });
1307
1292
  for (const [key, value] of Object.entries(row)) {
1293
+ if (isPrototypePollutingKey(key)) continue;
1308
1294
  const property = properties[key];
1309
1295
  const effectiveValue = foreignKeys.has(key) && value === "" ? null : value;
1310
1296
  if (!property) {
@@ -3796,51 +3782,6 @@ var FetchService = class {
3796
3782
  return !!this.getQueryBuilder(tableName);
3797
3783
  }
3798
3784
  /**
3799
- * Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
3800
- * for efficient JOIN-based relation loading.
3801
- * Returns null if the API is not available or the query fails.
3802
- * Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
3803
- */
3804
- async fetchWithDrizzleQuery(collectionPath, collection, options, include, idInfo, idInfoArray) {
3805
- try {
3806
- const table = getTableForCollection(collection, this.registry);
3807
- const tableName = getTableName(table);
3808
- const queryTarget = this.getQueryBuilder(tableName);
3809
- if (!queryTarget?.findMany) return null;
3810
- const resolvedRelations = resolveCollectionRelations(collection);
3811
- const withConfig = {};
3812
- for (const [key, relation] of Object.entries(resolvedRelations)) if (include[0] === "*" || include.includes(key)) {
3813
- const drizzleRelName = relation.relationName || key;
3814
- withConfig[drizzleRelName] = true;
3815
- }
3816
- const queryOpts = { with: withConfig };
3817
- if (options.limit) queryOpts.limit = options.limit;
3818
- if (options.filter) {
3819
- const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
3820
- if (filterConditions.length > 0) queryOpts.where = and(...filterConditions);
3821
- }
3822
- if (options.orderBy) {
3823
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3824
- if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
3825
- }
3826
- return (await queryTarget.findMany(queryOpts)).map((row) => {
3827
- const flat = {};
3828
- for (const [k, v] of Object.entries(row)) if (Array.isArray(v)) flat[k] = v.map((item) => {
3829
- const keys = Object.keys(item);
3830
- const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
3831
- if (nestedObj && keys.length <= 3) return { ...item[nestedObj] };
3832
- return { ...item };
3833
- });
3834
- else if (typeof v === "object" && v !== null) flat[k] = { ...v };
3835
- else flat[k] = v;
3836
- return flat;
3837
- });
3838
- } catch (e) {
3839
- logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
3840
- return null;
3841
- }
3842
- }
3843
- /**
3844
3785
  * Fallback path used when db.query is unavailable.
3845
3786
  * The primary path uses db.query.findMany with `with` config, which
3846
3787
  * loads all relations in a single query.
@@ -4091,7 +4032,8 @@ var PersistService = class {
4091
4032
  if (error instanceof ApiError || error?.name === "ApiError") return error;
4092
4033
  const pgError = extractPgError(error);
4093
4034
  if (pgError) {
4094
- const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
4035
+ const { message, code } = pgErrorToFriendlyMessage(pgError, collectionSlug);
4036
+ if (/^2[23]/.test(code)) return code === "23505" ? ApiError.conflict(message, `PG_${code}`) : ApiError.badRequest(message, `PG_${code}`);
4095
4037
  return new Error(message);
4096
4038
  }
4097
4039
  const causeMessage = extractCauseMessage(error);
@@ -7816,23 +7758,18 @@ var RealtimeService = class RealtimeService extends EventEmitter {
7816
7758
  path: request.path,
7817
7759
  collectionRequest: {
7818
7760
  filter: request.filter,
7761
+ logical: request.logical,
7819
7762
  orderBy: request.orderBy,
7820
7763
  order: request.order,
7821
7764
  limit: boundedLimit,
7765
+ offset: request.offset,
7822
7766
  startAfter: request.startAfter,
7823
7767
  databaseId: request.collection?.databaseId,
7824
7768
  searchString: request.searchString
7825
7769
  },
7826
7770
  authContext
7827
7771
  });
7828
- const rows = await this.fetchCollectionWithAuth(request.path, {
7829
- filter: request.filter,
7830
- orderBy: request.orderBy,
7831
- order: request.order,
7832
- limit: boundedLimit,
7833
- startAfter: request.startAfter,
7834
- searchString: request.searchString
7835
- }, authContext);
7772
+ const rows = await this.fetchCollectionWithAuth(request.path, this._subscriptions.get(subscriptionId).collectionRequest, authContext);
7836
7773
  this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
7837
7774
  } catch (error) {
7838
7775
  const sanitized = sanitizeErrorForClient(error, request.path);
@@ -8016,6 +7953,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8016
7953
  });
8017
7954
  else fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
8018
7955
  filter: collectionRequest.filter,
7956
+ logical: collectionRequest.logical,
8019
7957
  orderBy: collectionRequest.orderBy,
8020
7958
  order: collectionRequest.order,
8021
7959
  limit: collectionRequest.limit,
@@ -9203,6 +9141,54 @@ var quote = (xs) => Array.from(xs).map((s) => `\`${s}\``).join(", ");
9203
9141
  /** `on.from` / `on.to` accept a single column or a composite tuple. */
9204
9142
  var asColumns = (value) => Array.isArray(value) ? value : [value];
9205
9143
  /**
9144
+ * Distinguish "this column name is wrong" from "the generated schema is old".
9145
+ *
9146
+ * They present identically here — a relation asks for a column the registered
9147
+ * table does not have — but they are opposite problems with opposite fixes, and
9148
+ * getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked
9149
+ * projects.
9150
+ *
9151
+ * The registered table is not the database. It comes from the project's
9152
+ * checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that
9153
+ * derives foreign-key names: `categories` yields `category_id` where it used to
9154
+ * yield `categorie_id`. Boot-ensure renames the database column to match, so by
9155
+ * the time this runs the *database* is correct and the *generated module* is the
9156
+ * stale one. Reporting "not a column" then points at the wrong artifact, and the
9157
+ * generic fix — "set `through.targetColumn` to one of: …", listing the legacy
9158
+ * name because that is what the stale module still has — talks the reader into
9159
+ * pinning a column that no longer exists.
9160
+ *
9161
+ * So when the wanted name is what the current rule derives, and the table
9162
+ * carries what the *previous* rule would have derived from the same source, say
9163
+ * that instead.
9164
+ *
9165
+ * @param wanted the column the relation asks for
9166
+ * @param available every column the registered table has
9167
+ * @param sources names the default could have been derived from (a slug, a
9168
+ * relation name) — checking against these rather than guessing
9169
+ * backwards from `wanted` keeps the match exact
9170
+ */
9171
+ function staleCodegenRename(wanted, available, sources) {
9172
+ for (const source of sources) {
9173
+ if (!source) continue;
9174
+ const current = generateForeignKeyName(source);
9175
+ const legacy = legacyForeignKeyName(source);
9176
+ if (current !== wanted || legacy === current) continue;
9177
+ if (available.has(legacy) && !available.has(current)) return {
9178
+ legacy,
9179
+ current
9180
+ };
9181
+ }
9182
+ return null;
9183
+ }
9184
+ /** The shared explanation, so every relation kind reports it identically. */
9185
+ function staleCodegenDefect(table, { legacy, current }) {
9186
+ return {
9187
+ problem: `the generated Drizzle schema still declares \`${legacy}\` on \`${table}\`, but this release derives \`${current}\` — the generated schema predates the foreign-key naming fix and no longer describes the database`,
9188
+ fix: `regenerate it with \`rebase schema generate\` (or \`pnpm run schema:generate\`). The database column has already been renamed for you at boot, so nothing else is needed. To keep \`${legacy}\` instead, name it explicitly on the relation and regenerate.`
9189
+ };
9190
+ }
9191
+ /**
9206
9192
  * Relations whose names do not resolve against the registered schema.
9207
9193
  *
9208
9194
  * Fails open wherever it cannot see enough to be sure — an unregistered source
@@ -9249,19 +9235,31 @@ function findRelationDefects(collections, registry) {
9249
9235
  const targetColumns = columnNames(targetTable);
9250
9236
  switch (relation.kind) {
9251
9237
  case "belongsTo":
9252
- if (!sourceColumns.has(relation.localKey)) defects.push({
9253
- ...at,
9254
- problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
9255
- fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
9256
- });
9238
+ if (!sourceColumns.has(relation.localKey)) {
9239
+ const stale = staleCodegenRename(relation.localKey, sourceColumns, [relation.relationName, targetCollection.slug]);
9240
+ defects.push(stale ? {
9241
+ ...at,
9242
+ ...staleCodegenDefect(sourceTableName, stale)
9243
+ } : {
9244
+ ...at,
9245
+ problem: `\`localKey: "${relation.localKey}"\` is not a column on \`${sourceTableName}\``,
9246
+ fix: `add the column, or set \`localKey\` to one of: ${quote(sourceColumns)}`
9247
+ });
9248
+ }
9257
9249
  break;
9258
9250
  case "hasOne":
9259
9251
  case "hasMany":
9260
- if (!targetColumns.has(relation.foreignKeyOnTarget)) defects.push({
9261
- ...at,
9262
- problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
9263
- fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
9264
- });
9252
+ if (!targetColumns.has(relation.foreignKeyOnTarget)) {
9253
+ const stale = staleCodegenRename(relation.foreignKeyOnTarget, targetColumns, [collection.slug]);
9254
+ defects.push(stale ? {
9255
+ ...at,
9256
+ ...staleCodegenDefect(targetTableName, stale)
9257
+ } : {
9258
+ ...at,
9259
+ problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
9260
+ fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
9261
+ });
9262
+ }
9265
9263
  if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
9266
9264
  ...at,
9267
9265
  problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
@@ -9280,11 +9278,21 @@ function findRelationDefects(collections, registry) {
9280
9278
  break;
9281
9279
  }
9282
9280
  const junctionColumns = columnNames(junction);
9283
- for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) defects.push({
9284
- ...at,
9285
- problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
9286
- fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
9287
- });
9281
+ const derivedFrom = {
9282
+ sourceColumn: [collection.slug],
9283
+ targetColumn: [targetCollection.slug]
9284
+ };
9285
+ for (const [label, column] of [["sourceColumn", sourceColumn], ["targetColumn", targetColumn]]) if (!junctionColumns.has(column)) {
9286
+ const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);
9287
+ defects.push(stale ? {
9288
+ ...at,
9289
+ ...staleCodegenDefect(table, stale)
9290
+ } : {
9291
+ ...at,
9292
+ problem: `\`through.${label}: "${column}"\` is not a column on the junction table \`${table}\``,
9293
+ fix: `set \`through.${label}\` to one of: ${quote(junctionColumns)}` + (label === "sourceColumn" ? " — it is the column naming *this* collection" : "")
9294
+ });
9295
+ }
9288
9296
  break;
9289
9297
  }
9290
9298
  case "via": {
@@ -9356,7 +9364,16 @@ function assertRelationsResolve(collections, registry) {
9356
9364
  const defects = findRelationDefects(collections, registry);
9357
9365
  if (defects.length === 0) return;
9358
9366
  const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
9359
- throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against the database schema.\n\nEach of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
9367
+ throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against \`backend/src/schema.generated.ts\`.
9368
+
9369
+ Each of these would return no rows at query time rather than reporting an error, so they are fatal at boot instead.
9370
+
9371
+ If the database was migrated recently — an upgrade, a \`db push\`, a restore — this file is
9372
+ probably older than the schema it describes. Regenerate it before changing anything else:
9373
+
9374
+ rebase schema generate
9375
+
9376
+ If it is already current, then the collection is what disagrees with it:
9360
9377
 
9361
9378
  ` + lines.join("\n\n") + "\n");
9362
9379
  }
@@ -12288,7 +12305,7 @@ function createPostgresBootstrapper(pgConfig) {
12288
12305
  */
12289
12306
  async ensureCollectionSchema(collections, driverResult, log) {
12290
12307
  const internals = driverResult.internals;
12291
- const { ensureCollectionTables } = await import("./ensure-collection-tables-CBQdOETu.js");
12308
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-DRkgVBV5.js");
12292
12309
  const plan = await ensureCollectionTables({ async query(text) {
12293
12310
  const result = await internals.db.execute(sql.raw(text));
12294
12311
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
@@ -12313,7 +12330,7 @@ function createPostgresBootstrapper(pgConfig) {
12313
12330
  */
12314
12331
  async ensureCollectionPolicies(collections, driverResult, log) {
12315
12332
  const internals = driverResult.internals;
12316
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-ViG8XiPn.js");
12333
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-25nZOd87.js");
12317
12334
  const outcome = await ensureCollectionPolicies({ async query(text) {
12318
12335
  const result = await internals.db.execute(sql.raw(text));
12319
12336
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };