@rebasepro/server-postgres 0.13.1-canary.g18cfeb7 → 0.13.1-canary.g249daa1

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 (35) hide show
  1. package/dist/{auth-users-columns-Do9mw5Y5.js → auth-users-columns-Dt9g712t.js} +405 -7
  2. package/dist/auth-users-columns-Dt9g712t.js.map +1 -0
  3. package/dist/backup-service-Bww-Lg0s.js.map +1 -1
  4. package/dist/cli-helpers.d.ts +56 -0
  5. package/dist/{ensure-collection-policies-DMUdRQdM.js → ensure-collection-policies-CwYUliAa.js} +2 -2
  6. package/dist/{ensure-collection-policies-DMUdRQdM.js.map → ensure-collection-policies-CwYUliAa.js.map} +1 -1
  7. package/dist/{ensure-collection-tables-DSIxvLLD.js → ensure-collection-tables-DRxaUG96.js} +34 -7
  8. package/dist/ensure-collection-tables-DRxaUG96.js.map +1 -0
  9. package/dist/index.es.js +234 -26
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/schema/ensure-collection-tables.d.ts +1 -1
  12. package/dist/schema/generate-postgres-ddl-logic.d.ts +48 -0
  13. package/dist/schema/search-column.d.ts +199 -0
  14. package/dist/services/FetchService.d.ts +15 -0
  15. package/dist/services/dataService.d.ts +1 -0
  16. package/dist/services/realtimeService.d.ts +2 -0
  17. package/dist/src-C_wvdMnl.js.map +1 -1
  18. package/dist/utils/drizzle-conditions.d.ts +71 -2
  19. package/dist/websocket-D0TBU3ia.js.map +1 -1
  20. package/package.json +6 -6
  21. package/src/cli-helpers.ts +107 -1
  22. package/src/cli.ts +57 -0
  23. package/src/data-transformer.ts +18 -0
  24. package/src/schema/ensure-collection-tables.ts +77 -1
  25. package/src/schema/generate-drizzle-schema-logic.ts +25 -1
  26. package/src/schema/generate-postgres-ddl-logic.ts +176 -9
  27. package/src/schema/generate-postgres-ddl.ts +25 -2
  28. package/src/schema/search-column.ts +558 -0
  29. package/src/services/FetchService.ts +138 -15
  30. package/src/services/RelationService.ts +2 -1
  31. package/src/services/dataService.ts +1 -0
  32. package/src/services/realtimeService.ts +10 -4
  33. package/src/utils/drizzle-conditions.ts +223 -2
  34. package/dist/auth-users-columns-Do9mw5Y5.js.map +0 -1
  35. package/dist/ensure-collection-tables-DSIxvLLD.js.map +0 -1
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 generateForeignKeyName, B as hasForeignKeyOnTarget, C as getDeclaredPrimaryKeys, D as createRelationRefWithData, E as createRelationRef, F as camelCase, H as Vector, I as toSnakeCase, M as getPolicyNamesForRule, N as isPrototypePollutingKey, O as normalizeToEntityRelation, P as mergeDeep, S as buildCompositeId, T as parseIdValues, V as isManyToMany, _ as getColumnName, a as buildSdkData, b as getTableVarName, c as resolveStringColumnLength, d as resolveJunctionSpecs, f as getEffectiveSecurityRules, g as findRelation, h as findAnonymousGrants, j as legacyForeignKeyName, k as updateDateAutoValues, l as getJunctionCollectionConfig, m as securityRuleToConditions, o as CollectionRegistry, p as policyToPostgres, r as authUsersColumnSql, s as relationalCollections, t as AUTH_USERS_COLUMNS, u as getJunctionSecurityRules, v as getEnumVarName, w as isAddressableId, x as resolveCollectionRelations, y as getTableName$1, z as resolveClientListLimit } from "./auth-users-columns-Do9mw5Y5.js";
5
+ import { A as resolveCollectionRelations, B as legacyForeignKeyName, C as securityRuleToConditions, D as getEnumVarName, E as getColumnName, F as createRelationRef, G as camelCase, H as getPolicyNamesForRule, I as createRelationRefWithData, K as toSnakeCase, L as normalizeToEntityRelation, M as getDeclaredPrimaryKeys, N as isAddressableId, O as getTableName$1, P as parseIdValues, Q as Vector, R as updateDateAutoValues, S as policyToPostgres, T as findRelation, U as isPrototypePollutingKey, W as mergeDeep, X as hasForeignKeyOnTarget, Y as resolveClientListLimit, Z as isManyToMany, _ as resolveStringColumnLength, b as resolveJunctionSpecs, c as buildSearchColumnSpec, g as relationalCollections, h as CollectionRegistry, j as buildCompositeId, k as getTableVarName, l as hiddenColumnsOption, m as buildSdkData, o as SEARCH_UNACCENT_FN, p as visibleColumnProjection, r as authUsersColumnSql, t as AUTH_USERS_COLUMNS, v as getJunctionCollectionConfig, w as findAnonymousGrants, x as getEffectiveSecurityRules, y as getJunctionSecurityRules, z as generateForeignKeyName } from "./auth-users-columns-Dt9g712t.js";
6
6
  import { c as isPostgresCollectionConfig, l as isRelationalCollectionConfig, n as REBASE_SCHEMA, o as usesLegacyRlsFunctions } from "./src-C_wvdMnl.js";
7
7
  import { t as ANONYMOUS_USER_ID } from "./policy-CPkCqVTz.js";
8
8
  import { t as createPostgresWebSocket } from "./websocket-D0TBU3ia.js";
@@ -554,6 +554,12 @@ function deriveRowAddress(row, collection, registry) {
554
554
  //#endregion
555
555
  //#region src/utils/drizzle-conditions.ts
556
556
  /**
557
+ * Postgres's own default for `pg_trgm.word_similarity_threshold`. Named here
558
+ * because the fuzzy predicate has to know when the index-backed operator agrees
559
+ * with the collection's declared threshold and when it would narrow too far.
560
+ */
561
+ var PG_TRGM_WORD_SIMILARITY_DEFAULT = .6;
562
+ /**
557
563
  * Process-wide default, set once when the driver is constructed.
558
564
  *
559
565
  * The condition builder is a set of *static* methods reached from a dozen
@@ -611,7 +617,7 @@ function toMembershipList(value) {
611
617
  * @example
612
618
  * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
613
619
  */
614
- var DrizzleConditionBuilder = class {
620
+ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
615
621
  /**
616
622
  * Express "reachable from this parent through this relation" as a plain
617
623
  * `WHERE` condition on the target table.
@@ -1237,10 +1243,28 @@ var DrizzleConditionBuilder = class {
1237
1243
  return or(...conditions);
1238
1244
  }
1239
1245
  /**
1240
- * Build search conditions for text fields
1246
+ * Build search conditions for text fields.
1247
+ *
1248
+ * Two shapes, chosen by whether the collection declared a `search` block:
1249
+ *
1250
+ * - **Declared** — one `@@ websearch_to_tsquery` against the generated
1251
+ * `tsvector` column. Stems, drops stopwords, AND-es the terms, reaches
1252
+ * inside JSONB and arrays, and uses the GIN index.
1253
+ * - **Not declared** — the original `ILIKE '%term%'` OR-ed across top-level
1254
+ * string properties, unchanged.
1255
+ *
1256
+ * The second is the default and stays the default. A collection that has
1257
+ * not opted in compiles to exactly the SQL it compiled to before this
1258
+ * branch existed, which is the only reason it is safe to have added it.
1259
+ *
1260
+ * `collection` is optional so that the callers which genuinely have no
1261
+ * collection in hand — nested paths, derived views — keep working; without
1262
+ * one there is no `search` block to read and the ILIKE path is correct.
1241
1263
  */
1242
- static buildSearchConditions(searchString, properties, table) {
1264
+ static buildSearchConditions(searchString, properties, table, collection) {
1243
1265
  const searchConditions = [];
1266
+ const ftsCondition = collection ? DrizzleConditionBuilder.buildFullTextCondition(searchString, table, collection) : void 0;
1267
+ if (ftsCondition) return [ftsCondition];
1244
1268
  for (const [key, prop] of Object.entries(properties)) {
1245
1269
  const p = prop;
1246
1270
  if (p.type === "string" && !p.enum && p.isId !== "uuid") {
@@ -1253,6 +1277,117 @@ var DrizzleConditionBuilder = class {
1253
1277
  return searchConditions;
1254
1278
  }
1255
1279
  /**
1280
+ * The `@@` predicate for a collection that declared a `search` block, or
1281
+ * undefined for one that did not.
1282
+ *
1283
+ * The query is normalized exactly as the indexed content was — same text
1284
+ * search configuration, same accent folding. Skipping that on the query
1285
+ * side is the subtle way to get a search that matches nothing: the column
1286
+ * would hold `gestion` while the query asked for `gestión`.
1287
+ *
1288
+ * `websearch_to_tsquery` rather than `plainto_tsquery` because it is the
1289
+ * one that behaves the way a search box looks like it should — quoted
1290
+ * phrases, `or`, and a leading `-` to exclude — and because it never throws
1291
+ * on user input, which `to_tsquery` does on so much as a stray parenthesis.
1292
+ */
1293
+ static buildFullTextCondition(searchString, table, collection) {
1294
+ let spec;
1295
+ try {
1296
+ spec = buildSearchColumnSpec(collection);
1297
+ } catch {
1298
+ return;
1299
+ }
1300
+ if (!spec) return void 0;
1301
+ const column = table[spec.column];
1302
+ if (!column) return;
1303
+ const exact = sql`${column} @@ ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)}`;
1304
+ if (!spec.fuzzy) return exact;
1305
+ const fuzzyColumn = table[spec.fuzzy.column];
1306
+ if (!fuzzyColumn) return exact;
1307
+ const needle = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
1308
+ const similar = sql`public.word_similarity(${needle}, ${fuzzyColumn}) >= ${spec.fuzzy.threshold}`;
1309
+ return sql`(${exact} OR ${spec.fuzzy.threshold > PG_TRGM_WORD_SIMILARITY_DEFAULT ? sql`(${needle} OPERATOR(public.<%) ${fuzzyColumn} AND ${similar})` : similar})`;
1310
+ }
1311
+ /**
1312
+ * `websearch_to_tsquery(<config>, <normalized search string>)`.
1313
+ *
1314
+ * Split out because the ranking expression needs the identical query — a
1315
+ * row ranked against a different tsquery than it was matched against is a
1316
+ * ranking of something else.
1317
+ */
1318
+ static normalizedTsQuery(searchString, spec) {
1319
+ const normalized = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
1320
+ return sql`websearch_to_tsquery(${spec.language}, ${normalized})`;
1321
+ }
1322
+ /**
1323
+ * A JSONB array of `{ field, snippet }` naming which declared fields matched
1324
+ * and showing the text around each hit — what backs `_matches`.
1325
+ *
1326
+ * A ranked list answers "which rows", never "why this row". For a talent
1327
+ * pool that difference is the product: a candidate surfacing for
1328
+ * "iso 14001" on a *certification* is a different candidate from one whose
1329
+ * bio happens to mention the standard, and the score cannot tell them apart.
1330
+ *
1331
+ * Built as a correlated subquery over a `VALUES` list of the declared
1332
+ * fields, rather than one `CASE` per field, so the shape does not change
1333
+ * with the number of fields and the empty result is a plain `[]`.
1334
+ *
1335
+ * `ts_headline` runs over the same normalized text that was indexed. Over
1336
+ * the *original* text it would find nothing to mark whenever `unaccent` is
1337
+ * on — the query's lexemes are folded and the document's are not — and
1338
+ * would return the text silently unhighlighted. Folded-but-marked beats
1339
+ * pretty-but-inert.
1340
+ *
1341
+ * Undefined when the collection has not opted in, when the column is not on
1342
+ * the table yet, or when the caller did not ask: this costs a `ts_headline`
1343
+ * per field per row and `ts_headline` re-parses the document.
1344
+ */
1345
+ static buildSearchMatchesExpression(searchString, table, collection) {
1346
+ let spec;
1347
+ try {
1348
+ spec = buildSearchColumnSpec(collection);
1349
+ } catch {
1350
+ return;
1351
+ }
1352
+ if (!spec || spec.fields.length === 0) return void 0;
1353
+ if (!table[spec.column]) return void 0;
1354
+ const query = DrizzleConditionBuilder.normalizedTsQuery(searchString, spec);
1355
+ const config = sql`${spec.language}`;
1356
+ const rows = spec.fields.map((f, i) => sql`(${i}, ${f.path}, ${sql.raw(f.textSql)})`);
1357
+ return sql`(
1358
+ SELECT coalesce(jsonb_agg(s.m ORDER BY f.ord), '[]'::jsonb)
1359
+ FROM (VALUES ${sql.join(rows, sql`, `)}) AS f(ord, path, txt)
1360
+ CROSS JOIN LATERAL (
1361
+ SELECT jsonb_build_object(
1362
+ 'field', f.path,
1363
+ 'snippet', ts_headline(${config}::regconfig, f.txt, ${query},
1364
+ 'StartSel=<mark>,StopSel=</mark>,MaxWords=14,MinWords=1,MaxFragments=1,FragmentDelimiter= … ')
1365
+ ) AS m
1366
+ WHERE to_tsvector(${config}::regconfig, f.txt) @@ ${query}
1367
+ ) s
1368
+ )`;
1369
+ }
1370
+ /**
1371
+ * `ts_rank(<column>, <query>)` for the collection, or undefined when it has
1372
+ * not opted in. This is what backs `orderBy: ["_score", "desc"]`.
1373
+ */
1374
+ static buildSearchRankExpression(searchString, table, collection) {
1375
+ let spec;
1376
+ try {
1377
+ spec = buildSearchColumnSpec(collection);
1378
+ } catch {
1379
+ return;
1380
+ }
1381
+ if (!spec) return void 0;
1382
+ const column = table[spec.column];
1383
+ if (!column) return void 0;
1384
+ const rank = sql`ts_rank(${column}, ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)})`;
1385
+ if (!spec.fuzzy) return rank;
1386
+ const fuzzyColumn = table[spec.fuzzy.column];
1387
+ if (!fuzzyColumn) return rank;
1388
+ return sql`(${rank} + public.word_similarity(${spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`}, ${fuzzyColumn}))`;
1389
+ }
1390
+ /**
1256
1391
  * Build a unique field check condition
1257
1392
  */
1258
1393
  static buildUniqueFieldCondition(fieldColumn, value, idColumn, excludeId) {
@@ -1793,6 +1928,18 @@ function parsePropertyFromServer(value, property, collection, propertyKey) {
1793
1928
  * from the result (used by `normalizeDbValues` where
1794
1929
  * Drizzle's relational API already hydrates them).
1795
1930
  */
1931
+ /**
1932
+ * Keys a query computes and attaches to a row, rather than reads from a column.
1933
+ *
1934
+ * An explicit set rather than a `_` prefix rule: a user's column may perfectly
1935
+ * well be called `_internal`, and passing it through here would put a value on
1936
+ * the row that no property describes and nothing downstream knows how to type.
1937
+ */
1938
+ var QUERY_METADATA_KEYS = /* @__PURE__ */ new Set([
1939
+ "_score",
1940
+ "_distance",
1941
+ "_matches"
1942
+ ]);
1796
1943
  function normalizeScalarValues(data, properties, collection, resolvedRelations, options) {
1797
1944
  const result = {};
1798
1945
  const internalFKColumns = /* @__PURE__ */ new Set();
@@ -1804,6 +1951,10 @@ function normalizeScalarValues(data, properties, collection, resolvedRelations,
1804
1951
  result[key] = value === null ? null : typeof value === "number" ? value : String(value);
1805
1952
  continue;
1806
1953
  }
1954
+ if (QUERY_METADATA_KEYS.has(key)) {
1955
+ result[key] = value;
1956
+ continue;
1957
+ }
1807
1958
  const property = properties[key];
1808
1959
  if (!property) continue;
1809
1960
  if (options.skipRelations && property.type === "relation") continue;
@@ -2056,7 +2207,7 @@ var RelationService = class {
2056
2207
  let query = this.db.select().from(targetTable).$dynamic();
2057
2208
  const additionalFilters = [];
2058
2209
  if (options.searchString) {
2059
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable);
2210
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable, targetCollection);
2060
2211
  if (searchConditions.length === 0) return [];
2061
2212
  const searchCombined = DrizzleConditionBuilder.combineConditionsWithOr(searchConditions);
2062
2213
  if (searchCombined) additionalFilters.push(searchCombined);
@@ -3162,7 +3313,7 @@ function sanitizeErrorForClient(error, context) {
3162
3313
  * Service for handling all row read operations.
3163
3314
  * Handles fetching, searching, counting, and filtering rows.
3164
3315
  */
3165
- var FetchService = class {
3316
+ var FetchService = class FetchService {
3166
3317
  db;
3167
3318
  registry;
3168
3319
  relationService;
@@ -3235,6 +3386,24 @@ var FetchService = class {
3235
3386
  * and skips rows rather than erroring. The guesses stay, last, for a
3236
3387
  * caller that hands over no collection to resolve against.
3237
3388
  */
3389
+ /**
3390
+ * The ORDER BY target, which may be relevance rather than a column.
3391
+ *
3392
+ * `_score` is only meaningful for a collection that declared a `search`
3393
+ * block *and* for a request that carried a search string — ranking rows
3394
+ * against no query ranks them all at zero. Outside those two conditions it
3395
+ * is an unknown field and gets the same 400 as any other typo, which is the
3396
+ * behaviour that matters: a sort that is silently dropped returns 200 with
3397
+ * rows in arbitrary order, and paging over that repeats and skips rows.
3398
+ */
3399
+ static SCORE_FIELD = "_score";
3400
+ resolveOrderTarget(table, orderBy, collection, searchString) {
3401
+ if (orderBy === FetchService.SCORE_FIELD && collection && searchString) {
3402
+ const rank = DrizzleConditionBuilder.buildSearchRankExpression(searchString, table, collection);
3403
+ if (rank) return rank;
3404
+ }
3405
+ return this.resolveOrderByField(table, orderBy, collection);
3406
+ }
3238
3407
  resolveOrderByField(table, orderBy, collection) {
3239
3408
  const columnAt = (key) => (key in table ? table[key] : void 0) || void 0;
3240
3409
  const direct = columnAt(orderBy);
@@ -3362,12 +3531,14 @@ var FetchService = class {
3362
3531
  */
3363
3532
  buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig, scopeCondition) {
3364
3533
  const queryOpts = {};
3534
+ const hidden = hiddenColumnsOption(getTableColumns(table), this.registry.getCollectionByPath(collectionPath) ?? void 0);
3535
+ if (hidden) queryOpts.columns = hidden;
3365
3536
  if (withConfig) queryOpts.with = withConfig;
3366
3537
  const allConditions = [];
3367
3538
  if (scopeCondition) allConditions.push(scopeCondition);
3368
3539
  if (options.searchString) {
3369
3540
  const collection = getCollectionByPath(collectionPath, this.registry);
3370
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
3541
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3371
3542
  if (searchConditions.length === 0) {
3372
3543
  queryOpts.where = and(eq(idField, -99999999));
3373
3544
  return queryOpts;
@@ -3390,7 +3561,7 @@ var FetchService = class {
3390
3561
  const orderExpressions = [];
3391
3562
  if (options.orderBy) {
3392
3563
  const collection = getCollectionByPath(collectionPath, this.registry);
3393
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3564
+ const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3394
3565
  if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3395
3566
  }
3396
3567
  orderExpressions.push(desc(idField));
@@ -3407,6 +3578,7 @@ var FetchService = class {
3407
3578
  if (!options.startAfter) return [];
3408
3579
  const cursor = options.startAfter;
3409
3580
  if (options.orderBy) {
3581
+ if (options.orderBy === FetchService.SCORE_FIELD) throw ApiError.badRequest("Cursor pagination (`startAfter`) cannot be combined with `orderBy: \"_score\"`. Relevance is computed per query rather than stored, so it cannot key a cursor. Use `limit`/`offset` for relevance-ordered pages, or order by a column.", "SCORE_CURSOR_UNSUPPORTED", { field: FetchService.SCORE_FIELD });
3410
3582
  const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
3411
3583
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3412
3584
  if (orderByField) {
@@ -3477,9 +3649,11 @@ var FetchService = class {
3477
3649
  const qb = this.getQueryBuilder(tableName);
3478
3650
  if (qb) try {
3479
3651
  const withConfig = this.buildWithConfig(collection);
3652
+ const hidden = hiddenColumnsOption(getTableColumns(table), collection);
3480
3653
  const row = await qb.findFirst({
3481
3654
  where: eq(idField, parsedId),
3482
- with: withConfig
3655
+ with: withConfig,
3656
+ ...hidden ? { columns: hidden } : {}
3483
3657
  });
3484
3658
  if (!row) return void 0;
3485
3659
  const flatRow = toFlatRow(row, collection, this.registry);
@@ -3493,7 +3667,8 @@ var FetchService = class {
3493
3667
  if (reachedDatabase(e)) throw e;
3494
3668
  logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
3495
3669
  }
3496
- const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
3670
+ const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
3671
+ const result = await this.db.select(visibleOne).from(table).where(eq(idField, parsedId)).limit(1);
3497
3672
  if (result.length === 0) return void 0;
3498
3673
  const raw = result[0];
3499
3674
  const values = await parseDataFromServer(raw, collection, this.db, this.registry);
@@ -3549,14 +3724,21 @@ var FetchService = class {
3549
3724
  }
3550
3725
  let vectorMeta;
3551
3726
  if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
3727
+ const visible = visibleColumnProjection(getTableColumns(table), collection);
3728
+ const rankSelect = options.searchString ? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection) : void 0;
3729
+ const matchesSelect = options.searchString && options.searchExplain ? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection) : void 0;
3552
3730
  let query = vectorMeta ? this.db.select({
3553
- table_row: table,
3731
+ table_row: visible ?? table,
3554
3732
  _distance: vectorMeta.distanceSelect
3555
- }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
3733
+ }).from(table).$dynamic() : rankSelect ? this.db.select({
3734
+ table_row: visible ?? table,
3735
+ _score: rankSelect,
3736
+ ...matchesSelect ? { _matches: matchesSelect } : {}
3737
+ }).from(table).$dynamic() : visible ? this.db.select(visible).from(table).$dynamic() : this.db.select().from(table).$dynamic();
3556
3738
  const allConditions = [];
3557
3739
  if (scopeCondition) allConditions.push(scopeCondition);
3558
3740
  if (options.searchString) {
3559
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
3741
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3560
3742
  if (searchConditions.length === 0) return [];
3561
3743
  allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3562
3744
  }
@@ -3576,7 +3758,7 @@ var FetchService = class {
3576
3758
  const orderExpressions = [];
3577
3759
  if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
3578
3760
  else if (options.orderBy) {
3579
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
3761
+ const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3580
3762
  if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3581
3763
  }
3582
3764
  orderExpressions.push(desc(idField));
@@ -3596,6 +3778,10 @@ var FetchService = class {
3596
3778
  const results = vectorMeta ? rawResults.map((r) => ({
3597
3779
  ...r.table_row,
3598
3780
  _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
3781
+ })) : rankSelect ? rawResults.map((r) => ({
3782
+ ...r.table_row,
3783
+ _score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
3784
+ ...matchesSelect ? { _matches: r._matches ?? [] } : {}
3599
3785
  })) : rawResults;
3600
3786
  return this.processRowResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
3601
3787
  }
@@ -3687,7 +3873,7 @@ var FetchService = class {
3687
3873
  const allConditions = [];
3688
3874
  if (hop) allConditions.push(this.buildRelationScope(hop));
3689
3875
  if (options.searchString) {
3690
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
3876
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3691
3877
  if (searchConditions.length === 0) return 0;
3692
3878
  allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3693
3879
  }
@@ -3832,7 +4018,8 @@ var FetchService = class {
3832
4018
  if (reachedDatabase(e)) throw e;
3833
4019
  logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
3834
4020
  }
3835
- const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
4021
+ const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
4022
+ const result = await this.db.select(visibleOne).from(table).where(eq(idField, parsedId)).limit(1);
3836
4023
  if (result.length === 0) return null;
3837
4024
  const flatEntity = { ...result[0] };
3838
4025
  if (!include || include.length === 0) return flatEntity;
@@ -3870,14 +4057,21 @@ var FetchService = class {
3870
4057
  const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
3871
4058
  let vectorMeta;
3872
4059
  if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
4060
+ const visible = visibleColumnProjection(getTableColumns(table), collection);
4061
+ const rankSelect = options.searchString ? DrizzleConditionBuilder.buildSearchRankExpression(options.searchString, table, collection) : void 0;
4062
+ const matchesSelect = options.searchString && options.searchExplain ? DrizzleConditionBuilder.buildSearchMatchesExpression(options.searchString, table, collection) : void 0;
3873
4063
  let query = vectorMeta ? this.db.select({
3874
- table_row: table,
4064
+ table_row: visible ?? table,
3875
4065
  _distance: vectorMeta.distanceSelect
3876
- }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
4066
+ }).from(table).$dynamic() : rankSelect ? this.db.select({
4067
+ table_row: visible ?? table,
4068
+ _score: rankSelect,
4069
+ ...matchesSelect ? { _matches: matchesSelect } : {}
4070
+ }).from(table).$dynamic() : visible ? this.db.select(visible).from(table).$dynamic() : this.db.select().from(table).$dynamic();
3877
4071
  const allConditions = [];
3878
4072
  if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
3879
4073
  if (options.searchString) {
3880
- const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
4074
+ const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table, collection);
3881
4075
  if (searchConditions.length === 0) return [];
3882
4076
  allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
3883
4077
  }
@@ -3893,7 +4087,7 @@ var FetchService = class {
3893
4087
  const orderExpressions = [];
3894
4088
  if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
3895
4089
  else if (options.orderBy) {
3896
- const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
4090
+ const orderByField = this.resolveOrderTarget(table, options.orderBy, collection, options.searchString);
3897
4091
  if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
3898
4092
  }
3899
4093
  orderExpressions.push(desc(idField));
@@ -3906,6 +4100,11 @@ var FetchService = class {
3906
4100
  ...r.table_row,
3907
4101
  _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
3908
4102
  }));
4103
+ if (rankSelect) return rawResults.map((r) => ({
4104
+ ...r.table_row,
4105
+ _score: typeof r._score === "number" ? r._score : parseFloat(String(r._score)),
4106
+ ...matchesSelect ? { _matches: r._matches ?? [] } : {}
4107
+ }));
3909
4108
  return rawResults;
3910
4109
  }
3911
4110
  /**
@@ -6416,6 +6615,7 @@ var generateSchema = async (allCollections, stripPolicies = false) => {
6416
6615
  const hasUuid = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "string" && (p.autoValue === "uuid" || p.isId === "uuid")));
6417
6616
  const hasVector = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "vector"));
6418
6617
  const hasBinary = collections.some((c) => c.properties && Object.values(c.properties).some((p) => p.type === "binary"));
6618
+ const hasSearch = collections.some((c) => buildSearchColumnSpec(c) !== void 0);
6419
6619
  const pgCoreImports = [
6420
6620
  "primaryKey",
6421
6621
  "pgTable",
@@ -6440,7 +6640,7 @@ var generateSchema = async (allCollections, stripPolicies = false) => {
6440
6640
  ];
6441
6641
  if (hasUuid) pgCoreImports.push("uuid");
6442
6642
  if (hasVector) pgCoreImports.push("vector");
6443
- if (hasBinary) pgCoreImports.push("customType");
6643
+ if (hasBinary || hasSearch) pgCoreImports.push("customType");
6444
6644
  const uniqueSchemas = Array.from(new Set(collections.map((c) => isPostgresCollectionConfig(c) ? c.schema : void 0).filter(Boolean)));
6445
6645
  if (uniqueSchemas.length > 0) pgCoreImports.push("pgSchema");
6446
6646
  schemaContent += `import { ${pgCoreImports.join(", ")} } from 'drizzle-orm/pg-core';\n`;
@@ -6522,6 +6722,11 @@ var generateSchema = async (allCollections, stripPolicies = false) => {
6522
6722
  const columnString = getDrizzleColumn(propName, prop, collection, collections);
6523
6723
  if (columnString) columns.add(columnString);
6524
6724
  });
6725
+ const searchSpec = buildSearchColumnSpec(collection);
6726
+ if (searchSpec) {
6727
+ columns.add(` ${searchSpec.column}: customType({ dataType() { return 'tsvector'; } })("${searchSpec.column}").generatedAlwaysAs(sql\`${searchSpec.expression}\`)`);
6728
+ if (searchSpec.fuzzy) columns.add(` ${searchSpec.fuzzy.column}: text("${searchSpec.fuzzy.column}").generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`);
6729
+ }
6525
6730
  if (!Array.from(columns).some((col) => col.includes(".primaryKey()"))) columns.add(" id: text(\"id\").primaryKey()");
6526
6731
  schemaContent += `${Array.from(columns).join(",\n")}`;
6527
6732
  const securityRules = getEffectiveSecurityRules(collection);
@@ -7941,7 +8146,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
7941
8146
  limit: config.limit,
7942
8147
  startAfter: config.startAfter,
7943
8148
  databaseId: config.databaseId,
7944
- searchString: config.searchString
8149
+ searchString: config.searchString,
8150
+ searchExplain: config.searchExplain
7945
8151
  }
7946
8152
  });
7947
8153
  if (callback) this.subscriptionCallbacks.set(subscriptionId, callback);
@@ -8069,7 +8275,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8069
8275
  offset: request.offset,
8070
8276
  startAfter: request.startAfter,
8071
8277
  databaseId: request.collection?.databaseId,
8072
- searchString: request.searchString
8278
+ searchString: request.searchString,
8279
+ searchExplain: request.searchExplain
8073
8280
  },
8074
8281
  authContext
8075
8282
  });
@@ -8253,7 +8460,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
8253
8460
  orderBy: collectionRequest.orderBy,
8254
8461
  order: collectionRequest.order,
8255
8462
  limit: collectionRequest.limit,
8256
- databaseId: collectionRequest.databaseId
8463
+ databaseId: collectionRequest.databaseId,
8464
+ searchExplain: collectionRequest.searchExplain
8257
8465
  });
8258
8466
  else fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
8259
8467
  filter: collectionRequest.filter,
@@ -12611,7 +12819,7 @@ function createPostgresBootstrapper(pgConfig) {
12611
12819
  */
12612
12820
  async ensureCollectionSchema(collections, driverResult, log) {
12613
12821
  const internals = driverResult.internals;
12614
- const { ensureCollectionTables } = await import("./ensure-collection-tables-DSIxvLLD.js");
12822
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-DRxaUG96.js");
12615
12823
  const plan = await ensureCollectionTables({ async query(text) {
12616
12824
  const result = await internals.db.execute(sql.raw(text));
12617
12825
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
@@ -12636,7 +12844,7 @@ function createPostgresBootstrapper(pgConfig) {
12636
12844
  */
12637
12845
  async ensureCollectionPolicies(collections, driverResult, log) {
12638
12846
  const internals = driverResult.internals;
12639
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-DMUdRQdM.js");
12847
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-CwYUliAa.js");
12640
12848
  const outcome = await ensureCollectionPolicies({ async query(text) {
12641
12849
  const result = await internals.db.execute(sql.raw(text));
12642
12850
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };