@rebasepro/server-postgres 0.12.1-canary.g386667d → 0.12.1-canary.g4e7bcbf

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 (36) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/{backup-service-vAKWJkYL.js → backup-service-DLb2drIH.js} +7 -1
  3. package/dist/backup-service-DLb2drIH.js.map +1 -0
  4. package/dist/cli-helpers.d.ts +15 -0
  5. package/dist/{ensure-collection-policies-BjSwj0FM.js → ensure-collection-policies-Dv21KDMJ.js} +2 -2
  6. package/dist/{ensure-collection-policies-BjSwj0FM.js.map → ensure-collection-policies-Dv21KDMJ.js.map} +1 -1
  7. package/dist/{ensure-collection-tables-D6-XhqnQ.js → ensure-collection-tables-CT6xHB1d.js} +72 -12
  8. package/dist/ensure-collection-tables-CT6xHB1d.js.map +1 -0
  9. package/dist/index.es.js +74 -21
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/schema/ensure-collection-tables.d.ts +21 -1
  12. package/dist/schema/generate-postgres-ddl-logic.d.ts +19 -0
  13. package/dist/services/FetchService.d.ts +4 -1
  14. package/dist/services/dataService.d.ts +3 -1
  15. package/dist/services/row-pipeline.d.ts +1 -1
  16. package/dist/{src-C_NHNVW2.js → src-BkdpiQdw.js} +137 -18
  17. package/dist/src-BkdpiQdw.js.map +1 -0
  18. package/dist/{websocket-DB7TbFPT.js → websocket-B2LsrINK.js} +4 -3
  19. package/dist/websocket-B2LsrINK.js.map +1 -0
  20. package/package.json +6 -6
  21. package/src/PostgresAdapter.ts +7 -2
  22. package/src/PostgresBackendDriver.ts +4 -0
  23. package/src/cli-helpers.ts +26 -0
  24. package/src/cli.ts +2 -19
  25. package/src/schema/ensure-collection-tables.ts +81 -10
  26. package/src/schema/generate-postgres-ddl-logic.ts +50 -6
  27. package/src/schema/introspect-db-logic.ts +21 -3
  28. package/src/security/policy-drift.test.ts +11 -3
  29. package/src/services/FetchService.ts +76 -14
  30. package/src/services/dataService.ts +3 -1
  31. package/src/services/row-pipeline.ts +1 -1
  32. package/src/websocket.ts +15 -6
  33. package/dist/backup-service-vAKWJkYL.js.map +0 -1
  34. package/dist/ensure-collection-tables-D6-XhqnQ.js.map +0 -1
  35. package/dist/src-C_NHNVW2.js.map +0 -1
  36. package/dist/websocket-DB7TbFPT.js.map +0 -1
@@ -56,7 +56,7 @@ export interface ExistingSchema {
56
56
  constraints?: Set<string>;
57
57
  }
58
58
  export interface EnsureAction {
59
- kind: "create-enum" | "create-table" | "add-column" | "add-constraint";
59
+ kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column";
60
60
  /** Qualified target, for logging: `public.posts` or `public.posts.title`. */
61
61
  target: string;
62
62
  sql: string;
@@ -65,6 +65,26 @@ export interface EnsurePlan {
65
65
  actions: EnsureAction[];
66
66
  /** Every statement, in dependency order. Empty when the schema is current. */
67
67
  statements: string[];
68
+ /**
69
+ * Relation columns this plan is about to create where the table already
70
+ * carries the same column under its pre-singularization name.
71
+ *
72
+ * The reason this is reported rather than silently handled: the ensure is
73
+ * additive, so it would add `category_id` beside a populated
74
+ * `categorie_id` and the relation would then read the new, empty one. No
75
+ * statement fails, no table is missing, and the only symptom is relations
76
+ * resolving to nothing — which is indistinguishable from having no data.
77
+ */
78
+ legacyForeignKeys: LegacyForeignKey[];
79
+ }
80
+ /** A relation column whose old and new spellings both plausibly apply. */
81
+ export interface LegacyForeignKey {
82
+ /** `schema.table`. */
83
+ table: string;
84
+ /** The name the current rule derives, and what this plan would create. */
85
+ expected: string;
86
+ /** The name the old rule derived, which the table already has. */
87
+ legacy: string;
68
88
  }
69
89
  export interface EnsureOutcome extends EnsurePlan {
70
90
  /**
@@ -19,6 +19,15 @@ type ResolveCollection = (slug: string) => CollectionConfig | undefined;
19
19
  * multiple commands in one execute), while `db push` writes the joined string.
20
20
  */
21
21
  export declare const generatePolicyStatements: (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection) => string[];
22
+ /**
23
+ * Single-quote escaping for a SQL string literal (PostgreSQL doubles the
24
+ * quote). Enum labels come straight from user-authored collection config, so a
25
+ * label like `it's` closes the literal early and the whole generated file stops
26
+ * parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
27
+ * because ensure-collection-tables already imports from this module — the
28
+ * reverse would be a cycle.
29
+ */
30
+ export declare const quoteSqlLiteral: (value: string) => string;
22
31
  export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
23
32
  export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
24
33
  includePolicies?: boolean;
@@ -52,6 +61,15 @@ export interface RelationalColumnPlan {
52
61
  type: string;
53
62
  /** Absent when the target collection is not part of this bundle. */
54
63
  foreignKey?: ForeignKeyPlan;
64
+ /**
65
+ * What this column would have been called before `generateForeignKeyName`
66
+ * learned to singularize — set only when the two differ and the column name
67
+ * is the derived default rather than one the author wrote.
68
+ *
69
+ * Carried so the boot-time ensure can notice a database provisioned under
70
+ * the old rule. It is never used to name anything.
71
+ */
72
+ legacyColumn?: string;
55
73
  }
56
74
  /** The table behind a many-to-many `through` relation. */
57
75
  export interface JunctionTablePlan {
@@ -61,6 +79,7 @@ export interface JunctionTablePlan {
61
79
  columns: {
62
80
  name: string;
63
81
  type: string;
82
+ legacyName?: string;
64
83
  }[];
65
84
  /** Both endpoint columns plus the composite primary key. */
66
85
  createTable: string;
@@ -141,7 +141,7 @@ export declare class FetchService {
141
141
  /**
142
142
  * Fallback path used when db.query is unavailable.
143
143
  *
144
- * The primary path runs the results through `toCmsRow`, which maps
144
+ * The primary path runs the results through `toFlatRow`, which maps
145
145
  * relations from what drizzle already nested — no query per row. This one
146
146
  * has no nesting to read, so it resolves relations itself, in batches.
147
147
  *
@@ -177,6 +177,7 @@ export declare class FetchService {
177
177
  */
178
178
  count<M extends Record<string, unknown>>(collectionPath: string, options?: {
179
179
  filter?: FilterValues<Extract<keyof M, string>>;
180
+ logical?: LogicalCondition;
180
181
  searchString?: string;
181
182
  databaseId?: string;
182
183
  }): Promise<number>;
@@ -198,6 +199,8 @@ export declare class FetchService {
198
199
  */
199
200
  fetchCollectionForRest<M extends Record<string, unknown>>(collectionPath: string, options?: {
200
201
  filter?: FilterValues<Extract<keyof M, string>>;
202
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
203
+ logical?: LogicalCondition;
201
204
  orderBy?: string;
202
205
  order?: "desc" | "asc";
203
206
  limit?: number;
@@ -1,4 +1,4 @@
1
- import { FilterValues } from "@rebasepro/types";
1
+ import { FilterValues, LogicalCondition } from "@rebasepro/types";
2
2
  import type { VectorSearchParams } from "@rebasepro/types";
3
3
  import { FetchService } from "./FetchService";
4
4
  import { PersistService } from "./PersistService";
@@ -60,6 +60,8 @@ export declare class DataService implements DataRepository {
60
60
  */
61
61
  count<M extends Record<string, unknown>>(collectionPath: string, options?: {
62
62
  filter?: FilterValues<Extract<keyof M, string>>;
63
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
64
+ logical?: LogicalCondition;
63
65
  searchString?: string;
64
66
  databaseId?: string;
65
67
  }): Promise<number>;
@@ -47,7 +47,7 @@ export declare function relationTargetAddress(targetRow: Record<string, unknown>
47
47
  * expects real types. The row's own address is *not* among the columns — it is
48
48
  * derived by the consumer from the collection's primary keys.
49
49
  */
50
- export declare function toCmsRow(row: Record<string, unknown>, collection: CollectionConfig, registry: PostgresCollectionRegistry): Record<string, unknown>;
50
+ export declare function toFlatRow(row: Record<string, unknown>, collection: CollectionConfig, registry: PostgresCollectionRegistry): Record<string, unknown>;
51
51
  /**
52
52
  * The row REST serves: every column under its own name, with the value Postgres
53
53
  * returned, and relations inlined as the target's columns.
@@ -1067,16 +1067,12 @@ function removeFunctions(o) {
1067
1067
  if (o === void 0) return void 0;
1068
1068
  if (o === null) return null;
1069
1069
  if (typeof o === "object") {
1070
- if (Array.isArray(o)) return o.map((v) => removeFunctions(v));
1070
+ if (Array.isArray(o)) return o.filter((v) => typeof v !== "function").map((v) => removeFunctions(v));
1071
1071
  if (!isPlainObject(o)) return o;
1072
- return Object.entries(o).filter(([_, value]) => typeof value !== "function").map(([key, value]) => {
1073
- if (Array.isArray(value)) return { [key]: value.map((v) => removeFunctions(v)) };
1074
- else if (typeof value === "object") return { [key]: removeFunctions(value) };
1075
- else return { [key]: value };
1076
- }).reduce((a, b) => ({
1077
- ...a,
1078
- ...b
1079
- }), {});
1072
+ return Object.entries(o).filter(([_, value]) => typeof value !== "function").reduce((acc, [key, value]) => {
1073
+ acc[key] = removeFunctions(value);
1074
+ return acc;
1075
+ }, {});
1080
1076
  }
1081
1077
  return o;
1082
1078
  }
@@ -1215,11 +1211,101 @@ function getPolicyNamesForRule(rule, tableName) {
1215
1211
  return ops.map((op, opIdx) => rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`);
1216
1212
  }
1217
1213
  //#endregion
1214
+ //#region ../utils/src/plurals.ts
1215
+ /**
1216
+ * Returns the singular of an English word.
1217
+ *
1218
+ * @param {string} word
1219
+ * @param {number} [amount]
1220
+ * @returns {string}
1221
+ */
1222
+ function singular(word, amount) {
1223
+ if (amount !== void 0 && amount !== 1) return word;
1224
+ const singulars = {
1225
+ "(quiz)zes$": "$1",
1226
+ "(matr)ices$": "$1ix",
1227
+ "(vert|ind)ices$": "$1ex",
1228
+ "^(ox)en$": "$1",
1229
+ "(alias)es$": "$1",
1230
+ "(octop|vir)i$": "$1us",
1231
+ "(cris|ax|test)es$": "$1is",
1232
+ "(shoe)s$": "$1",
1233
+ "(o)es$": "$1",
1234
+ "(bus)es$": "$1",
1235
+ "([m|l])ice$": "$1ouse",
1236
+ "(x|ch|ss|sh)es$": "$1",
1237
+ "(m)ovies$": "$1ovie",
1238
+ "(s)eries$": "$1eries",
1239
+ "([^aeiouy]|qu)ies$": "$1y",
1240
+ "([lr])ves$": "$1f",
1241
+ "(tive)s$": "$1",
1242
+ "(hive)s$": "$1",
1243
+ "(li|wi|kni)ves$": "$1fe",
1244
+ "(shea|loa|lea|thie)ves$": "$1f",
1245
+ "(^analy)ses$": "$1sis",
1246
+ "((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": "$1$2sis",
1247
+ "([ti])a$": "$1um",
1248
+ "(n)ews$": "$1ews",
1249
+ "(h|bl)ouses$": "$1ouse",
1250
+ "(corpse)s$": "$1",
1251
+ "(us)es$": "$1",
1252
+ s$: ""
1253
+ };
1254
+ const irregular = {
1255
+ move: "moves",
1256
+ foot: "feet",
1257
+ goose: "geese",
1258
+ sex: "sexes",
1259
+ child: "children",
1260
+ man: "men",
1261
+ tooth: "teeth",
1262
+ person: "people"
1263
+ };
1264
+ if ([
1265
+ "sheep",
1266
+ "fish",
1267
+ "deer",
1268
+ "moose",
1269
+ "series",
1270
+ "species",
1271
+ "money",
1272
+ "rice",
1273
+ "information",
1274
+ "equipment",
1275
+ "bison",
1276
+ "cod",
1277
+ "offspring",
1278
+ "pike",
1279
+ "salmon",
1280
+ "shrimp",
1281
+ "swine",
1282
+ "trout",
1283
+ "aircraft",
1284
+ "hovercraft",
1285
+ "spacecraft",
1286
+ "sugar",
1287
+ "tuna",
1288
+ "you",
1289
+ "wood"
1290
+ ].indexOf(word.toLowerCase()) >= 0) return word;
1291
+ for (const w in irregular) {
1292
+ const pattern = new RegExp(`${irregular[w]}$`, "i");
1293
+ if (pattern.test(word)) return word.replace(pattern, w);
1294
+ }
1295
+ for (const reg in singulars) {
1296
+ const pattern = new RegExp(reg, "i");
1297
+ if (pattern.test(word)) return word.replace(pattern, singulars[reg]);
1298
+ }
1299
+ return word;
1300
+ }
1301
+ //#endregion
1218
1302
  //#region ../utils/src/names.ts
1219
1303
  /**
1220
1304
  * Generates a foreign key column name from a given string, typically a collection slug or name.
1221
- * It converts the name to snake_case, attempts to singularize it by removing a trailing 's'
1222
- * (a common convention for collection names), and appends '_id'.
1305
+ * It singularizes the name, converts it to snake_case and appends '_id'.
1306
+ *
1307
+ * Singularization runs *before* snake-casing so that acronyms survive: `toSnakeCase`
1308
+ * splits on every capital, which turned "URLs" into "ur_ls" and then "ur_l_id".
1223
1309
  *
1224
1310
  * @param name The base name to convert to a foreign key.
1225
1311
  * @returns A foreign key name in the format 'singular_name_id'.
@@ -1229,8 +1315,8 @@ function getPolicyNamesForRule(rule, tableName) {
1229
1315
  * generateForeignKeyName("users")
1230
1316
  *
1231
1317
  * @example
1232
- * // returns "post_id"
1233
- * generateForeignKeyName("posts")
1318
+ * // returns "category_id"
1319
+ * generateForeignKeyName("categories")
1234
1320
  *
1235
1321
  * @example
1236
1322
  * // returns "product_id"
@@ -1238,8 +1324,41 @@ function getPolicyNamesForRule(rule, tableName) {
1238
1324
  *
1239
1325
  */
1240
1326
  function generateForeignKeyName(name) {
1241
- const snakeCaseName = toSnakeCase(name);
1242
- return `${snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName}_id`;
1327
+ return `${toSnakeCase(singularizeForKey(name))}_id`;
1328
+ }
1329
+ /**
1330
+ * `singular()` handles real English plurals, but its final catch-all rule strips
1331
+ * any trailing "s", which mangles words that only look plural. Guard the two
1332
+ * cases that produce a column name nobody would recognise:
1333
+ *
1334
+ * - a double "s" ending is never a plural marker ("address", "class", "process"),
1335
+ * so stripping it yields "addres";
1336
+ * - a name that singularizes to nothing (the literal "s") would yield "_id".
1337
+ */
1338
+ function singularizeForKey(name) {
1339
+ if (/ss$/i.test(name)) return name;
1340
+ const result = singular(name);
1341
+ return result.length > 0 ? result : name;
1342
+ }
1343
+ /**
1344
+ * What `generateForeignKeyName` returned before it learned to singularize:
1345
+ * snake-case the name, then chop one trailing "s".
1346
+ *
1347
+ * This is here to be *detected*, never to be generated. A database provisioned
1348
+ * under the old rule carries `categorie_id`, `addresse_id`, `children_id` or
1349
+ * `ur_l_id` where the current rule expects `category_id`, `address_id`,
1350
+ * `child_id` and `url_id` — and the boot-time schema ensure is additive, so it
1351
+ * would create the new column empty beside the populated old one and leave the
1352
+ * relation reading nothing. No error, no missing table: the failure is silent,
1353
+ * which is the only reason this function still exists.
1354
+ *
1355
+ * `ensureCollectionSchema` calls it to recognise that shape and say so.
1356
+ * Returns the same string as `generateForeignKeyName` for every regular plural,
1357
+ * so a caller can compare the two and act only when they differ.
1358
+ */
1359
+ function legacyForeignKeyName(name) {
1360
+ const snake = toSnakeCase(name);
1361
+ return `${snake.endsWith("s") ? snake.slice(0, -1) : snake}_id`;
1243
1362
  }
1244
1363
  //#endregion
1245
1364
  //#region ../common/src/util/entities.ts
@@ -3731,7 +3850,7 @@ function rowToEntity(row, slug, primaryKeys = []) {
3731
3850
  };
3732
3851
  }
3733
3852
  /**
3734
- * The relation envelope `toCmsRow` writes where a relation was:
3853
+ * The relation envelope `toFlatRow` writes where a relation was:
3735
3854
  * `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
3736
3855
  * admin's view-model, and the only pipeline that produces one is postgres'.
3737
3856
  */
@@ -4109,6 +4228,6 @@ function buildSdkData(driver) {
4109
4228
  return wrapAsSdkData(buildRebaseData(driver));
4110
4229
  }
4111
4230
  //#endregion
4112
- export { DEFAULT_ONE_OF_VALUE as A, updateDateAutoValues as C, camelCase as D, mergeDeep as E, isManyToMany as M, Vector as N, toSnakeCase as O, normalizeToEntityRelation as S, getPolicyNamesForRule as T, buildCompositeId as _, getJunctionSecurityRules as a, createRelationRef as b, policyToPostgres as c, findRelation as d, getColumnName as f, resolveCollectionRelations as g, getTableVarName as h, getJunctionCollectionConfig as i, hasForeignKeyOnTarget as j, DEFAULT_ONE_OF_TYPE as k, securityRuleToConditions as l, getTableName as m, CollectionRegistry as n, resolveJunctionSpecs as o, getEnumVarName as p, resolveStringColumnLength as r, getEffectiveSecurityRules as s, buildSdkData as t, findAnonymousGrants as u, getDeclaredPrimaryKeys as v, generateForeignKeyName as w, createRelationRefWithData as x, parseIdValues as y };
4231
+ export { DEFAULT_ONE_OF_TYPE as A, updateDateAutoValues as C, mergeDeep as D, getPolicyNamesForRule as E, hasForeignKeyOnTarget as M, isManyToMany as N, camelCase as O, Vector as P, normalizeToEntityRelation as S, legacyForeignKeyName as T, buildCompositeId as _, getJunctionSecurityRules as a, createRelationRef as b, policyToPostgres as c, findRelation as d, getColumnName as f, resolveCollectionRelations as g, getTableVarName as h, getJunctionCollectionConfig as i, DEFAULT_ONE_OF_VALUE as j, toSnakeCase as k, securityRuleToConditions as l, getTableName as m, CollectionRegistry as n, resolveJunctionSpecs as o, getEnumVarName as p, resolveStringColumnLength as r, getEffectiveSecurityRules as s, buildSdkData as t, findAnonymousGrants as u, getDeclaredPrimaryKeys as v, generateForeignKeyName as w, createRelationRefWithData as x, parseIdValues as y };
4113
4232
 
4114
- //# sourceMappingURL=src-C_NHNVW2.js.map
4233
+ //# sourceMappingURL=src-BkdpiQdw.js.map