@rebasepro/server-postgres 0.21.2-canary.g1ea48be → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{backup-cli-CkjsJEcu.js → backup-cli-DqBakiMO.js} +2 -2
- package/dist/{backup-cli-CkjsJEcu.js.map → backup-cli-DqBakiMO.js.map} +1 -1
- package/dist/{backup-service-BNLwvxuy.js → backup-service-CaMOS76G.js} +2 -2
- package/dist/{backup-service-BNLwvxuy.js.map → backup-service-CaMOS76G.js.map} +1 -1
- package/dist/cli.js +9 -9
- package/dist/{doctor-Gzzd8wTq.js → doctor-CU9IogdL.js} +3 -3
- package/dist/{doctor-Gzzd8wTq.js.map → doctor-CU9IogdL.js.map} +1 -1
- package/dist/{ensure-collection-policies-B4IDFtQ-.js → ensure-collection-policies-Dzd-2S81.js} +4 -4
- package/dist/{ensure-collection-policies-B4IDFtQ-.js.map → ensure-collection-policies-Dzd-2S81.js.map} +1 -1
- package/dist/{ensure-collection-tables-Cr2ye5JC.js → ensure-collection-tables-CpAgy51F.js} +3 -3
- package/dist/{ensure-collection-tables-Cr2ye5JC.js.map → ensure-collection-tables-CpAgy51F.js.map} +1 -1
- package/dist/{ensure-tables-BnEvEJPr.js → ensure-tables-Cr5B4UmH.js} +2 -2
- package/dist/{ensure-tables-BnEvEJPr.js.map → ensure-tables-Cr5B4UmH.js.map} +1 -1
- package/dist/{generate-drizzle-schema-D2MDdJt-.js → generate-drizzle-schema-B537GIvz.js} +2 -2
- package/dist/{generate-drizzle-schema-D2MDdJt-.js.map → generate-drizzle-schema-B537GIvz.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js → generate-drizzle-schema-logic-BcMl7VSy.js} +2 -2
- package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js.map → generate-drizzle-schema-logic-BcMl7VSy.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js → generate-postgres-ddl-logic-D7imhYV8.js} +2 -2
- package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js.map → generate-postgres-ddl-logic-D7imhYV8.js.map} +1 -1
- package/dist/index.es.js +391 -54
- package/dist/index.es.js.map +1 -1
- package/dist/{plan-schema-Hgl62S-w.js → plan-schema-CboAIwLN.js} +13 -8
- package/dist/plan-schema-CboAIwLN.js.map +1 -0
- package/dist/{policy-drift-B0GDRh7_.js → policy-drift-xJfy9xG7.js} +2 -2
- package/dist/{policy-drift-B0GDRh7_.js.map → policy-drift-xJfy9xG7.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-BHA6jLtj.js → rls-bootstrap-sql-H3rCFi3F.js} +5 -5
- package/dist/{rls-bootstrap-sql-BHA6jLtj.js.map → rls-bootstrap-sql-H3rCFi3F.js.map} +1 -1
- package/dist/schema/doctor-cli.js +2 -2
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/search-column.d.ts +21 -1
- package/dist/services/FetchService.d.ts +50 -2
- package/dist/services/PersistService.d.ts +16 -1
- package/dist/services/RelationService.d.ts +31 -1
- package/dist/services/RelationWriteService.d.ts +16 -1
- package/dist/services/dataService.d.ts +16 -1
- package/dist/services/read-scope.d.ts +100 -0
- package/dist/utils/drizzle-conditions.d.ts +56 -6
- package/package.json +7 -7
- package/dist/plan-schema-Hgl62S-w.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -2,25 +2,25 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
4
|
import { a as createDirectDatabaseConnection, c as guardPoolAgainstDirtyRelease, l as pinSearchPath, o as createPostgresDatabaseConnection, r as cappedPoolMax, s as createReadReplicaConnection, t as DatabasePoolManager, u as poolMaxCeiling } from "./databasePoolManager-Bj5FbeAs.js";
|
|
5
|
-
import { f as hiddenColumnsOption, l as SEARCH_UNACCENT_FN, m as searchColumnNames, o as sharedRelationName, u as buildSearchColumnSpec, x as visibleColumnProjection } from "./plan-schema-
|
|
5
|
+
import { f as hiddenColumnsOption, l as SEARCH_UNACCENT_FN, m as searchColumnNames, o as sharedRelationName, u as buildSearchColumnSpec, x as visibleColumnProjection } from "./plan-schema-CboAIwLN.js";
|
|
6
6
|
import { d as extractPgError, f as isRoleSwitchingPermissionError, g as sanitizeErrorForClient, h as reachedDatabase, i as diagnoseDbError, l as classifyConnectFailure, m as pgErrorToFriendlyMessage, n as deepestErrorMessage, o as isEconnrefused, p as isRowLevelSecurityDenial, s as parseHostInfo, u as extractCauseMessage } from "./cli-errors-Dka89exj.js";
|
|
7
7
|
import { r as BranchingUnsupportedError, t as BranchService } from "./BranchService-CucnFcSE.js";
|
|
8
8
|
import { c as warnOnLegacyRlsFunctions, i as ensureAppRole, l as warnOnRoleSchemaCollision, n as applyAuthContext, o as validatePolicyPgRoles, r as detectConnectionPosture, s as warnOnAnonymousGrants, t as REBASE_USER_ROLE } from "./rls-enforcement-C6Xk0lA6.js";
|
|
9
9
|
import { l as resolveColumnName } from "./column-plan-helpers-CpILzHJS.js";
|
|
10
10
|
import { i as isAuthCollection, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-D2LBFrMH.js";
|
|
11
|
-
import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "./generate-postgres-ddl-logic-
|
|
12
|
-
import { t as generateSchema } from "./generate-drizzle-schema-logic-
|
|
13
|
-
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-
|
|
14
|
-
import { t as main } from "./generate-drizzle-schema-
|
|
15
|
-
import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as getServerVersionMajor, d as pruneBackups, f as resolvePgBinary, g as selectBackupsToPrune, h as validateDump, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as listBackups, m as uploadBackup, n as applyGlobals, o as discardPartialDump, p as restoreDump, s as ensureDatabaseExists, t as BackupToolError, u as preflight, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-
|
|
16
|
-
import { i as resolveAuthSchema, r as probeAuthSchema, t as ensureAuthTablesExist } from "./ensure-tables-
|
|
11
|
+
import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "./generate-postgres-ddl-logic-D7imhYV8.js";
|
|
12
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-BcMl7VSy.js";
|
|
13
|
+
import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-CpAgy51F.js";
|
|
14
|
+
import { t as main } from "./generate-drizzle-schema-B537GIvz.js";
|
|
15
|
+
import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as getServerVersionMajor, d as pruneBackups, f as resolvePgBinary, g as selectBackupsToPrune, h as validateDump, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as listBackups, m as uploadBackup, n as applyGlobals, o as discardPartialDump, p as restoreDump, s as ensureDatabaseExists, t as BackupToolError, u as preflight, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-CaMOS76G.js";
|
|
16
|
+
import { i as resolveAuthSchema, r as probeAuthSchema, t as ensureAuthTablesExist } from "./ensure-tables-Cr5B4UmH.js";
|
|
17
17
|
import { c as identifyJoinTables, d as singularize, h as humanize, i as buildTablesMap, m as mapPgType, r as buildEnumMap, s as getIconForTable } from "./introspect-db-logic-C6LQdTxj.js";
|
|
18
18
|
import { Client } from "pg";
|
|
19
19
|
import { ApiError, assertFieldOpsValid, assertWriteRequestValid, createDdlBootstrapper, createEmailService, declaredErrorAnswer, extractUserFromToken, logger, rawQueryLoggingEnabled, resolveBatchRefs, resolveRequireAuth, safeCompare, splitFieldOps } from "@rebasepro/server";
|
|
20
20
|
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, isNotNull, isNull, isTable, lt, not, notInArray, or, relations, sql } from "drizzle-orm";
|
|
21
21
|
import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, declaredDatabaseExtensions, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit } from "@rebasepro/types";
|
|
22
22
|
import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE as REBASE_USER_ROLE$1, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createRelationRef, createRelationRefWithData, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionCollectionConfig, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseOrderBySpecStrict, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveJunctionSpecs, resolveTenantWrite, revokeInternalTableSql, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, updateDateAutoValues, updateUserAutoValues } from "@rebasepro/common";
|
|
23
|
-
import { camelCase, firstFreeKey, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, mergeDeep, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
|
|
23
|
+
import { camelCase, firstFreeKey, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, mergeDeep, splitSearchTerms, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
|
|
24
24
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
25
|
import { PgArray, PgNumeric, PgTable, bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, getTableConfig, index, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, unique, uuid, varchar, vector } from "drizzle-orm/pg-core";
|
|
26
26
|
import fs from "fs";
|
|
@@ -408,6 +408,15 @@ function deriveRowAddress(row, collection, registry) {
|
|
|
408
408
|
*/
|
|
409
409
|
var PG_TRGM_WORD_SIMILARITY_DEFAULT = .6;
|
|
410
410
|
/**
|
|
411
|
+
* What a substring-only hit contributes to `_score` under
|
|
412
|
+
* {@link SearchMode} `"hybrid"`.
|
|
413
|
+
*
|
|
414
|
+
* Below the smallest `ts_rank` a real lexeme match can produce (a single
|
|
415
|
+
* D-weighted hit is 0.1 × 0.0607927 ≈ 0.006), so a row found by the indexed
|
|
416
|
+
* half always outranks one found only by substring.
|
|
417
|
+
*/
|
|
418
|
+
var HYBRID_SUBSTRING_SCORE = .001;
|
|
419
|
+
/**
|
|
411
420
|
* A user's search term, made safe to drop inside a `%…%` LIKE pattern.
|
|
412
421
|
*
|
|
413
422
|
* The term is already a bind parameter, so this is not about injection. It is
|
|
@@ -1693,13 +1702,20 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
|
|
|
1693
1702
|
* - **Declared** — one `@@ websearch_to_tsquery` against the generated
|
|
1694
1703
|
* `tsvector` column. Stems, drops stopwords, AND-es the terms, reaches
|
|
1695
1704
|
* inside JSONB and arrays, and uses the GIN index.
|
|
1696
|
-
* - **Not declared** —
|
|
1697
|
-
* string properties, with
|
|
1698
|
-
* so it is matched as the literal text
|
|
1705
|
+
* - **Not declared** — `ILIKE '%term%'` per term, OR-ed across top-level
|
|
1706
|
+
* string properties and AND-ed across the terms, with each term escaped
|
|
1707
|
+
* (see {@link escapeLikePattern}) so it is matched as the literal text
|
|
1708
|
+
* the user typed.
|
|
1699
1709
|
*
|
|
1700
|
-
* The second is the default and stays the default
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1710
|
+
* The second is the default and stays the default, and it AND-es terms for
|
|
1711
|
+
* the same reason `websearch_to_tsquery` does: a person typing two words
|
|
1712
|
+
* into a search box means both, and a name lives in two columns. Matching
|
|
1713
|
+
* the whole string per column instead — which is what this did — cannot
|
|
1714
|
+
* find `sebastian melendez` on a row with `first_name`/`last_name`, and
|
|
1715
|
+
* could not find `sebastian ` at all.
|
|
1716
|
+
*
|
|
1717
|
+
* Returns at most one condition. Every caller ORs what it gets back, which
|
|
1718
|
+
* is right across columns and wrong across terms, so the AND is built here.
|
|
1703
1719
|
*
|
|
1704
1720
|
* `collection` is optional so that the callers which genuinely have no
|
|
1705
1721
|
* collection in hand — nested paths, derived views — keep working; without
|
|
@@ -1711,16 +1727,24 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
|
|
|
1711
1727
|
if (ftsCondition) return [ftsCondition];
|
|
1712
1728
|
let declaredStringProperties = 0;
|
|
1713
1729
|
const viewer = currentFieldViewer();
|
|
1730
|
+
const searchableColumns = [];
|
|
1714
1731
|
for (const [key, prop] of Object.entries(properties)) {
|
|
1715
1732
|
const p = prop;
|
|
1716
1733
|
if (!canReadField(prop, viewer)) continue;
|
|
1717
1734
|
if (p.type === "string" && !p.enum && p.isId !== "uuid") {
|
|
1718
1735
|
declaredStringProperties++;
|
|
1719
1736
|
const fieldColumn = table[key];
|
|
1720
|
-
if (fieldColumn && supportsILike(fieldColumn))
|
|
1737
|
+
if (fieldColumn && supportsILike(fieldColumn)) searchableColumns.push(fieldColumn);
|
|
1721
1738
|
}
|
|
1722
1739
|
}
|
|
1723
|
-
if (declaredStringProperties > 0 &&
|
|
1740
|
+
if (declaredStringProperties > 0 && searchableColumns.length === 0) logger.warn(`[search] "${collection?.slug ?? "collection"}" declares ${declaredStringProperties} string property(ies) but none compiled to a searchable column, so this search can only return nothing. Check that the generated schema's column types are text/varchar/char.`);
|
|
1741
|
+
if (searchableColumns.length === 0) return [];
|
|
1742
|
+
const terms = splitSearchTerms(searchString);
|
|
1743
|
+
const perTerm = (terms.length > 0 ? terms : [searchString]).map((term) => {
|
|
1744
|
+
const pattern = `%${escapeLikePattern(term)}%`;
|
|
1745
|
+
return DrizzleConditionBuilder.combineConditionsWithOr(searchableColumns.map((column) => ilike(column, pattern)));
|
|
1746
|
+
});
|
|
1747
|
+
searchConditions.push(DrizzleConditionBuilder.combineConditionsWithAnd(perTerm));
|
|
1724
1748
|
return searchConditions;
|
|
1725
1749
|
}
|
|
1726
1750
|
/**
|
|
@@ -1748,12 +1772,66 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
|
|
|
1748
1772
|
const column = table[spec.column];
|
|
1749
1773
|
if (!column) return;
|
|
1750
1774
|
const exact = sql`${column} @@ ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)}`;
|
|
1751
|
-
|
|
1775
|
+
const substring = spec.mode === "hybrid" ? DrizzleConditionBuilder.buildFoldedSubstringCondition(searchString, spec) : void 0;
|
|
1776
|
+
const matched = substring ? sql`(${exact} OR ${substring})` : exact;
|
|
1777
|
+
if (!spec.fuzzy) return matched;
|
|
1752
1778
|
const fuzzyColumn = table[spec.fuzzy.column];
|
|
1753
|
-
if (!fuzzyColumn) return
|
|
1779
|
+
if (!fuzzyColumn) return matched;
|
|
1754
1780
|
const needle = spec.unaccent ? sql`${sql.raw(SEARCH_UNACCENT_FN)}(${searchString})` : sql`${searchString}`;
|
|
1755
1781
|
const similar = sql`public.word_similarity(${needle}, ${fuzzyColumn}) >= ${spec.fuzzy.threshold}`;
|
|
1756
|
-
return sql`(${
|
|
1782
|
+
return sql`(${matched} OR ${spec.fuzzy.threshold > PG_TRGM_WORD_SIMILARITY_DEFAULT ? sql`(${needle} OPERATOR(public.<%) ${fuzzyColumn} AND ${similar})` : similar})`;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* The substring half of {@link SearchMode} `"hybrid"`: every term found
|
|
1786
|
+
* somewhere in the declared fields, with accents folded on both sides.
|
|
1787
|
+
*
|
|
1788
|
+
* Terms are AND-ed and fields OR-ed, exactly as the ILIKE default does and
|
|
1789
|
+
* for the same reason — a person typing two words means both, and a name
|
|
1790
|
+
* lives in two columns (`splitSearchTerms`). What it does *not* share with
|
|
1791
|
+
* the default is its reach: these are the declared `search.fields`, so it
|
|
1792
|
+
* folds accents, joins `text[]` and walks into JSONB through the same
|
|
1793
|
+
* IMMUTABLE helpers the generated column uses.
|
|
1794
|
+
*
|
|
1795
|
+
* The needle is folded with the same function as the document, so
|
|
1796
|
+
* `munoz` and `Muñoz` meet in the middle rather than one side being
|
|
1797
|
+
* normalized and the other not — the subtle way to build a search that
|
|
1798
|
+
* matches nothing.
|
|
1799
|
+
*
|
|
1800
|
+
* Interpolated as raw SQL because the field expressions are generated (they
|
|
1801
|
+
* name columns and helper functions, which are identifiers, not values);
|
|
1802
|
+
* the *term* is a bind parameter, escaped for LIKE first — see
|
|
1803
|
+
* {@link escapeLikePattern} for what that escape is for.
|
|
1804
|
+
*/
|
|
1805
|
+
static buildFoldedSubstringCondition(searchString, spec) {
|
|
1806
|
+
if (spec.fields.length === 0) return void 0;
|
|
1807
|
+
const perTerm = DrizzleConditionBuilder.foldedNeedles(searchString).map((needle) => DrizzleConditionBuilder.combineConditionsWithOr(spec.fields.map((f) => sql`${sql.raw(f.foldedTextSql)} ILIKE ${needle}`)));
|
|
1808
|
+
return DrizzleConditionBuilder.combineConditionsWithAnd(perTerm);
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Whether one text expression holds **any** term of the search string.
|
|
1812
|
+
*
|
|
1813
|
+
* The per-field question, where {@link buildFoldedSubstringCondition} asks
|
|
1814
|
+
* the per-row one: `OR` across terms rather than `AND`, because a two-word
|
|
1815
|
+
* query satisfied across two fields has each field carrying one term. Used
|
|
1816
|
+
* by `_matches` to name the fields that caused a substring hit.
|
|
1817
|
+
*/
|
|
1818
|
+
static foldedSubstringOf(searchString, text) {
|
|
1819
|
+
return DrizzleConditionBuilder.combineConditionsWithOr(DrizzleConditionBuilder.foldedNeedles(searchString).map((needle) => sql`${text} ILIKE ${needle}`));
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* One accent-folded `%term%` bind parameter per search term.
|
|
1823
|
+
*
|
|
1824
|
+
* The needle is folded with the same function as the document, so `munoz`
|
|
1825
|
+
* and `Muñoz` meet in the middle instead of one side being normalized and
|
|
1826
|
+
* the other not — the subtle way to build a search that matches nothing.
|
|
1827
|
+
*
|
|
1828
|
+
* A string with no terms at all is only whitespace, and stays the single
|
|
1829
|
+
* literal pattern it is on the ILIKE path rather than becoming "no rows",
|
|
1830
|
+
* so both paths answer a stray space the same way.
|
|
1831
|
+
*/
|
|
1832
|
+
static foldedNeedles(searchString) {
|
|
1833
|
+
const terms = splitSearchTerms(searchString);
|
|
1834
|
+
return (terms.length > 0 ? terms : [searchString]).map((term) => sql`${sql.raw(SEARCH_UNACCENT_FN)}(${`%${escapeLikePattern(term)}%`})`);
|
|
1757
1835
|
}
|
|
1758
1836
|
/**
|
|
1759
1837
|
* `websearch_to_tsquery(<config>, <normalized search string>)`.
|
|
@@ -1800,17 +1878,18 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
|
|
|
1800
1878
|
if (!table[spec.column]) return void 0;
|
|
1801
1879
|
const query = DrizzleConditionBuilder.normalizedTsQuery(searchString, spec);
|
|
1802
1880
|
const config = sql`${spec.language}`;
|
|
1803
|
-
const rows = spec.fields.map((f, i) => sql`(${i}, ${f.path}, ${sql.raw(f.textSql)})`);
|
|
1881
|
+
const rows = spec.fields.map((f, i) => sql`(${i}, ${f.path}, ${sql.raw(f.textSql)}, ${sql.raw(f.foldedTextSql)})`);
|
|
1882
|
+
const matchedField = spec.mode === "hybrid" ? sql`(to_tsvector(${config}::regconfig, f.txt) @@ ${query} OR ${DrizzleConditionBuilder.foldedSubstringOf(searchString, sql`f.folded`)})` : sql`to_tsvector(${config}::regconfig, f.txt) @@ ${query}`;
|
|
1804
1883
|
return sql`(
|
|
1805
1884
|
SELECT coalesce(jsonb_agg(s.m ORDER BY f.ord), '[]'::jsonb)
|
|
1806
|
-
FROM (VALUES ${sql.join(rows, sql`, `)}) AS f(ord, path, txt)
|
|
1885
|
+
FROM (VALUES ${sql.join(rows, sql`, `)}) AS f(ord, path, txt, folded)
|
|
1807
1886
|
CROSS JOIN LATERAL (
|
|
1808
1887
|
SELECT jsonb_build_object(
|
|
1809
1888
|
'field', f.path,
|
|
1810
1889
|
'snippet', ts_headline(${config}::regconfig, f.txt, ${query},
|
|
1811
1890
|
'StartSel=<mark>,StopSel=</mark>,MaxWords=14,MinWords=1,MaxFragments=1,FragmentDelimiter= … ')
|
|
1812
1891
|
) AS m
|
|
1813
|
-
WHERE
|
|
1892
|
+
WHERE ${matchedField}
|
|
1814
1893
|
) s
|
|
1815
1894
|
)`;
|
|
1816
1895
|
}
|
|
@@ -1828,7 +1907,11 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
|
|
|
1828
1907
|
if (!spec) return void 0;
|
|
1829
1908
|
const column = table[spec.column];
|
|
1830
1909
|
if (!column) return void 0;
|
|
1831
|
-
const
|
|
1910
|
+
const base = sql`ts_rank(${column}, ${DrizzleConditionBuilder.normalizedTsQuery(searchString, spec)})`;
|
|
1911
|
+
const rank = spec.mode === "hybrid" ? (() => {
|
|
1912
|
+
const substring = DrizzleConditionBuilder.buildFoldedSubstringCondition(searchString, spec);
|
|
1913
|
+
return substring ? sql`(${base} + CASE WHEN ${substring} THEN ${sql.raw(`${HYBRID_SUBSTRING_SCORE}::real`)} ELSE 0 END)` : base;
|
|
1914
|
+
})() : base;
|
|
1832
1915
|
if (!spec.fuzzy) return rank;
|
|
1833
1916
|
const fuzzyColumn = table[spec.fuzzy.column];
|
|
1834
1917
|
if (!fuzzyColumn) return rank;
|
|
@@ -2488,6 +2571,124 @@ function andSoftDelete(where, collection, table, withDeleted) {
|
|
|
2488
2571
|
return where ? and(where, condition) : condition;
|
|
2489
2572
|
}
|
|
2490
2573
|
//#endregion
|
|
2574
|
+
//#region src/services/read-scope.ts
|
|
2575
|
+
/**
|
|
2576
|
+
* `beforeQuery`: the one place a collection callback can change *which* rows a
|
|
2577
|
+
* read asks for.
|
|
2578
|
+
*
|
|
2579
|
+
* `afterRead` runs over rows that have already been fetched, so until this
|
|
2580
|
+
* existed an application developer who needed the read itself narrowed — a
|
|
2581
|
+
* tenant scope, a visibility window, a per-role row filter — had two options:
|
|
2582
|
+
* patch this driver, or `rebase eject`.
|
|
2583
|
+
*
|
|
2584
|
+
* Two properties are load-bearing, and both belong to the *shape* of the thing
|
|
2585
|
+
* rather than to how carefully each call site was written.
|
|
2586
|
+
*
|
|
2587
|
+
* ## It can only narrow
|
|
2588
|
+
*
|
|
2589
|
+
* A hook returns a {@link QueryNarrowing} — a declarative filter, optionally a
|
|
2590
|
+
* logical group — which is AND-ed into the query the caller sent. `AND(q, c)`
|
|
2591
|
+
* is a subset of `q` for every `c`, so there is no `c` a hook can return that
|
|
2592
|
+
* widens a read. The obvious alternative shape, `(query) => query`, can: a
|
|
2593
|
+
* hook that drops a condition on the way through runs the query without it,
|
|
2594
|
+
* and on an RLS data plane a dropped condition returns everything the policies
|
|
2595
|
+
* happen to allow. That is the same reasoning behind
|
|
2596
|
+
* `UnknownFilterFieldsMode` defaulting to `"error"`, and it is why that mode is
|
|
2597
|
+
* **forced** to `"error"` for a hook's own filter here whatever the
|
|
2598
|
+
* process-wide setting is: a scope condition naming a renamed column has to
|
|
2599
|
+
* refuse the request, never be dropped from it.
|
|
2600
|
+
*
|
|
2601
|
+
* ## It cannot be skipped
|
|
2602
|
+
*
|
|
2603
|
+
* The hooks are resolved *by the read path itself*, out of the registry that
|
|
2604
|
+
* read already holds — not handed to it by a caller who might forget. So a
|
|
2605
|
+
* read that compiles a WHERE compiles this one too, or it throws.
|
|
2606
|
+
*
|
|
2607
|
+
* The one thing a read path cannot derive is the identity the hook runs as:
|
|
2608
|
+
* `FetchService`, `RelationService` and `DataService` carry no user. That
|
|
2609
|
+
* arrives as a {@link ReadCallContextProvider}, threaded from the driver that
|
|
2610
|
+
* constructed them — the driver is the only object that knows both the caller
|
|
2611
|
+
* and the connection, and it constructs a service per transaction anyway, so
|
|
2612
|
+
* there is exactly one place per instance to pass it.
|
|
2613
|
+
*
|
|
2614
|
+
* Absence is the interesting case, and it fails closed: a service built with no
|
|
2615
|
+
* provider refuses any read of a collection that declares `beforeQuery`, with
|
|
2616
|
+
* a message naming the collection. So a future construction site that forgets
|
|
2617
|
+
* the provider breaks loudly on the first scoped collection instead of quietly
|
|
2618
|
+
* serving every row — which is the failure mode this whole module exists to
|
|
2619
|
+
* rule out. A deployment with no `beforeQuery` anywhere never reaches any of
|
|
2620
|
+
* this: the check is one map lookup and the compiled SQL is byte-identical.
|
|
2621
|
+
*
|
|
2622
|
+
* @module
|
|
2623
|
+
*/
|
|
2624
|
+
/**
|
|
2625
|
+
* The `beforeQuery` hooks that apply to one collection, in the order they run.
|
|
2626
|
+
*
|
|
2627
|
+
* Global first, then the collection's own — the same order `afterRead` and
|
|
2628
|
+
* `beforeSave` use. The order changes nothing about the outcome here (both are
|
|
2629
|
+
* AND-ed in, so neither can relax the other) and is kept because a hook that
|
|
2630
|
+
* logs or measures should see the same sequence everywhere.
|
|
2631
|
+
*
|
|
2632
|
+
* There is no property tier: a property callback shapes one value on a row, and
|
|
2633
|
+
* a row filter is not a value.
|
|
2634
|
+
*/
|
|
2635
|
+
function resolveBeforeQuery(registry, collection) {
|
|
2636
|
+
const hooks = [];
|
|
2637
|
+
const global = registry?.getGlobalCallbacks();
|
|
2638
|
+
if (global?.beforeQuery) hooks.push(global.beforeQuery);
|
|
2639
|
+
const own = collection?.callbacks;
|
|
2640
|
+
if (own?.beforeQuery) hooks.push(own.beforeQuery);
|
|
2641
|
+
return hooks;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* The conditions every `beforeQuery` hook for this collection asks to add.
|
|
2645
|
+
*
|
|
2646
|
+
* Returned as a list rather than pre-combined so a caller pushes them onto the
|
|
2647
|
+
* same `allConditions` array its own filter goes into. Every caller AND-es
|
|
2648
|
+
* that array, which is what makes these additive — the hook is not trusted to
|
|
2649
|
+
* narrow, it is only ever given a way to.
|
|
2650
|
+
*/
|
|
2651
|
+
async function beforeQueryConditions(env, collection, path, table, description, filterContext) {
|
|
2652
|
+
const hooks = resolveBeforeQuery(env.registry, collection);
|
|
2653
|
+
if (hooks.length === 0) return [];
|
|
2654
|
+
if (!collection) throw new Error(`[beforeQuery] "${path}" resolves to no registered collection, so a global \`beforeQuery\` cannot be compiled against it. Register the collection, or narrow the hook to the collections it applies to.`);
|
|
2655
|
+
if (!env.callContext) throw new Error(`[beforeQuery] "${collection.slug}" declares a \`beforeQuery\` hook, and this read was compiled by a service that was built without a call-context provider, so the hook could not run. Serving the read unnarrowed is not an option — a hook that does not run widens the result set. Construct the service through the driver (which passes one), or pass a provider explicitly.`);
|
|
2656
|
+
const context = env.callContext();
|
|
2657
|
+
const unknownFields = "error";
|
|
2658
|
+
const options = {
|
|
2659
|
+
...filterContext,
|
|
2660
|
+
unknownFields
|
|
2661
|
+
};
|
|
2662
|
+
const conditions = [];
|
|
2663
|
+
for (const hook of hooks) {
|
|
2664
|
+
const narrowing = await hook({
|
|
2665
|
+
collection,
|
|
2666
|
+
path,
|
|
2667
|
+
operation: description.operation,
|
|
2668
|
+
query: description.query,
|
|
2669
|
+
context
|
|
2670
|
+
});
|
|
2671
|
+
if (!narrowing) continue;
|
|
2672
|
+
if (!narrowing.filter && !narrowing.logical) throw new Error(`[beforeQuery] "${collection.slug}" returned an object with neither \`filter\` nor \`logical\`. Return nothing (or \`undefined\`) to add no conditions; an empty object reads as a scope that was meant to be built and was not.`);
|
|
2673
|
+
if (narrowing.filter) conditions.push(...DrizzleConditionBuilder.buildFilterConditions(narrowing.filter, table, path, options));
|
|
2674
|
+
if (narrowing.logical) {
|
|
2675
|
+
const logical = DrizzleConditionBuilder.buildLogicalConditions(narrowing.logical, table, path, options);
|
|
2676
|
+
if (logical) conditions.push(logical);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return conditions;
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* {@link beforeQueryConditions}, pre-combined, for the read paths that hold a
|
|
2683
|
+
* single `SQL` rather than a list of them — a single get, a relation loader.
|
|
2684
|
+
*/
|
|
2685
|
+
async function beforeQueryCondition(env, collection, path, table, description, filterContext) {
|
|
2686
|
+
const conditions = await beforeQueryConditions(env, collection, path, table, description, filterContext);
|
|
2687
|
+
if (conditions.length === 0) return void 0;
|
|
2688
|
+
if (conditions.length === 1) return conditions[0];
|
|
2689
|
+
return and(...conditions);
|
|
2690
|
+
}
|
|
2691
|
+
//#endregion
|
|
2491
2692
|
//#region src/services/RelationService.ts
|
|
2492
2693
|
/**
|
|
2493
2694
|
* Typed wrapper for Drizzle dynamic query innerJoin.
|
|
@@ -2522,6 +2723,17 @@ function narrowed(base, narrow) {
|
|
|
2522
2723
|
return narrow ? and(base, narrow) : base;
|
|
2523
2724
|
}
|
|
2524
2725
|
/**
|
|
2726
|
+
* Two optional conditions, AND-ed: either, both, or neither.
|
|
2727
|
+
*
|
|
2728
|
+
* The include's own `where` and the target collection's `beforeQuery` arrive
|
|
2729
|
+
* separately and are both optional, and they have to reach the loaders as one
|
|
2730
|
+
* condition because that is the shape every relation kind threads through.
|
|
2731
|
+
*/
|
|
2732
|
+
function narrowBoth(a, b) {
|
|
2733
|
+
if (a && b) return and(a, b);
|
|
2734
|
+
return a ?? b;
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2525
2737
|
* The registry has no table for a collection a relation just asked about.
|
|
2526
2738
|
*
|
|
2527
2739
|
* "Parent table not found" was the whole message, four times over, and it names
|
|
@@ -2538,9 +2750,43 @@ function unregisteredTable(collectionSlug, table, role) {
|
|
|
2538
2750
|
var RelationService = class {
|
|
2539
2751
|
db;
|
|
2540
2752
|
registry;
|
|
2541
|
-
|
|
2753
|
+
callContext;
|
|
2754
|
+
constructor(db, registry, callContext) {
|
|
2542
2755
|
this.db = db;
|
|
2543
2756
|
this.registry = registry;
|
|
2757
|
+
this.callContext = callContext;
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* The **target** collection's `beforeQuery` narrowing, for rows reached
|
|
2761
|
+
* through a relation.
|
|
2762
|
+
*
|
|
2763
|
+
* The target's, not the parent's, because these are the target's rows: a
|
|
2764
|
+
* scope on `comments` has to hold whether they are listed at
|
|
2765
|
+
* `/comments` or loaded as `post.comments`. Without this an `include` is a
|
|
2766
|
+
* way around every row filter a collection declares — the exact hole
|
|
2767
|
+
* `stripUnreadable` was once missing from the relation-ref branch.
|
|
2768
|
+
*
|
|
2769
|
+
* Folded into the `narrow` condition the loaders already thread through
|
|
2770
|
+
* every relation kind, so a `via` join, a junction, a foreign key and the
|
|
2771
|
+
* dynamic relation builder all apply it or none of them do.
|
|
2772
|
+
*/
|
|
2773
|
+
narrowTargetRead(parentCollection, relation, parentId) {
|
|
2774
|
+
const targetCollection = relation.target();
|
|
2775
|
+
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
2776
|
+
return beforeQueryCondition({
|
|
2777
|
+
registry: this.registry,
|
|
2778
|
+
callContext: this.callContext
|
|
2779
|
+
}, targetCollection, targetCollection.slug, targetTable, {
|
|
2780
|
+
operation: "relation",
|
|
2781
|
+
query: { relatedTo: {
|
|
2782
|
+
parentSlug: parentCollection.slug,
|
|
2783
|
+
parentId,
|
|
2784
|
+
relationName: relation.relationName
|
|
2785
|
+
} }
|
|
2786
|
+
}, {
|
|
2787
|
+
collection: targetCollection,
|
|
2788
|
+
registry: this.registry
|
|
2789
|
+
});
|
|
2544
2790
|
}
|
|
2545
2791
|
/**
|
|
2546
2792
|
* One target row, as the {@link RelatedRow} everything here returns.
|
|
@@ -2711,6 +2957,7 @@ var RelationService = class {
|
|
|
2711
2957
|
const parentTable = this.registry.getTable(parentTableName);
|
|
2712
2958
|
if (!parentTable) throw unregisteredTable(parentCollection.slug, parentTableName, "parent");
|
|
2713
2959
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
2960
|
+
const relatedNarrowing = await this.narrowTargetRead(parentCollection, relation, parentId);
|
|
2714
2961
|
if (relation.kind === "via") {
|
|
2715
2962
|
let query = this.db.select().from(parentTable).$dynamic();
|
|
2716
2963
|
let currentTable = parentTable;
|
|
@@ -2730,7 +2977,7 @@ var RelationService = class {
|
|
|
2730
2977
|
currentTable = joinTable;
|
|
2731
2978
|
}
|
|
2732
2979
|
const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
2733
|
-
query = query.where(eq(parentIdField, parsedParentId));
|
|
2980
|
+
query = query.where(narrowed(eq(parentIdField, parsedParentId), relatedNarrowing));
|
|
2734
2981
|
if (options.limit) query = query.limit(options.limit);
|
|
2735
2982
|
const results = await query;
|
|
2736
2983
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
@@ -2747,6 +2994,7 @@ var RelationService = class {
|
|
|
2747
2994
|
const additionalFilters = [];
|
|
2748
2995
|
const relatedSoftDelete = softDeleteCondition(targetCollection, targetTable);
|
|
2749
2996
|
if (relatedSoftDelete) additionalFilters.push(relatedSoftDelete);
|
|
2997
|
+
if (relatedNarrowing) additionalFilters.push(relatedNarrowing);
|
|
2750
2998
|
if (options.searchString) {
|
|
2751
2999
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, targetCollection.properties, targetTable, targetCollection);
|
|
2752
3000
|
if (searchConditions.length === 0) return [];
|
|
@@ -2801,6 +3049,8 @@ var RelationService = class {
|
|
|
2801
3049
|
if (matchValue === void 0) return 0;
|
|
2802
3050
|
const countSoftDelete = softDeleteCondition(targetCollection, targetTable);
|
|
2803
3051
|
if (countSoftDelete) additionalFilters = [...additionalFilters, countSoftDelete];
|
|
3052
|
+
const countNarrowing = await this.narrowTargetRead(parentCollection, relation, parentId);
|
|
3053
|
+
if (countNarrowing) additionalFilters = [...additionalFilters, countNarrowing];
|
|
2804
3054
|
let query = this.db.select({ count: sql`count(distinct ${targetIdField})` }).from(targetTable).$dynamic();
|
|
2805
3055
|
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation, matchValue, targetTable, parentTable, parentIdCol, targetIdField, this.registry, additionalFilters);
|
|
2806
3056
|
const result = await query;
|
|
@@ -2848,7 +3098,7 @@ var RelationService = class {
|
|
|
2848
3098
|
if (!parentTable) throw unregisteredTable(parentCollection.slug, parentTableName, "parent");
|
|
2849
3099
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
2850
3100
|
const parsedParentIds = parentIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
|
|
2851
|
-
const narrowTarget = andSoftDelete(narrow, targetCollection, targetTable);
|
|
3101
|
+
const narrowTarget = andSoftDelete(narrowBoth(narrow, await this.narrowTargetRead(parentCollection, relation)), targetCollection, targetTable);
|
|
2852
3102
|
if (relation.kind === "via") {
|
|
2853
3103
|
let query = this.db.select().from(parentTable).$dynamic();
|
|
2854
3104
|
let currentTable = parentTable;
|
|
@@ -2952,7 +3202,7 @@ var RelationService = class {
|
|
|
2952
3202
|
if (!parentTable) throw unregisteredTable(parentCollection.slug, parentTableName, "parent");
|
|
2953
3203
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
2954
3204
|
const parsedParentIds = parentIds.map((id) => parseIdValues(id, parentPks)[parentIdInfo.fieldName]);
|
|
2955
|
-
const narrowTarget = andSoftDelete(narrow, targetCollection, targetTable);
|
|
3205
|
+
const narrowTarget = andSoftDelete(narrowBoth(narrow, await this.narrowTargetRead(parentCollection, relation)), targetCollection, targetTable);
|
|
2956
3206
|
if (relation.kind === "via") {
|
|
2957
3207
|
let query = this.db.select().from(parentTable).$dynamic();
|
|
2958
3208
|
let currentTable = parentTable;
|
|
@@ -3652,11 +3902,13 @@ function numericAggregateAliases(aggregates, properties) {
|
|
|
3652
3902
|
var FetchService = class FetchService {
|
|
3653
3903
|
db;
|
|
3654
3904
|
registry;
|
|
3905
|
+
callContext;
|
|
3655
3906
|
relationService;
|
|
3656
|
-
constructor(db, registry) {
|
|
3907
|
+
constructor(db, registry, callContext) {
|
|
3657
3908
|
this.db = db;
|
|
3658
3909
|
this.registry = registry;
|
|
3659
|
-
this.
|
|
3910
|
+
this.callContext = callContext;
|
|
3911
|
+
this.relationService = new RelationService(db, registry, callContext);
|
|
3660
3912
|
}
|
|
3661
3913
|
/**
|
|
3662
3914
|
* Get the relational query builder for a given table name.
|
|
@@ -3690,6 +3942,63 @@ var FetchService = class FetchService {
|
|
|
3690
3942
|
};
|
|
3691
3943
|
}
|
|
3692
3944
|
/**
|
|
3945
|
+
* The conditions this collection's `beforeQuery` hooks ask to add, for a
|
|
3946
|
+
* read that is about to be compiled.
|
|
3947
|
+
*
|
|
3948
|
+
* Resolved here, from the registry this service already holds, rather than
|
|
3949
|
+
* handed in by a caller — so a read path that builds a WHERE cannot serve
|
|
3950
|
+
* one of these collections without the hook. `[]` when nothing is declared,
|
|
3951
|
+
* which leaves the compiled SQL byte-identical.
|
|
3952
|
+
*
|
|
3953
|
+
* Every caller pushes the result onto the same `allConditions` array its
|
|
3954
|
+
* own filter goes into, and every one of those arrays is AND-ed. That is
|
|
3955
|
+
* what keeps a hook additive: it is not trusted to narrow, it is only ever
|
|
3956
|
+
* given a way to.
|
|
3957
|
+
*/
|
|
3958
|
+
narrowRead(collectionPath, collection, table, operation, query) {
|
|
3959
|
+
return beforeQueryConditions({
|
|
3960
|
+
registry: this.registry,
|
|
3961
|
+
callContext: this.callContext
|
|
3962
|
+
}, collection, collectionPath, table, {
|
|
3963
|
+
operation,
|
|
3964
|
+
query
|
|
3965
|
+
}, this.filterContext(collectionPath, table));
|
|
3966
|
+
}
|
|
3967
|
+
/** {@link narrowRead}, pre-combined, for the reads that hold one `SQL`. */
|
|
3968
|
+
narrowReadCondition(collectionPath, collection, table, operation, query) {
|
|
3969
|
+
return beforeQueryCondition({
|
|
3970
|
+
registry: this.registry,
|
|
3971
|
+
callContext: this.callContext
|
|
3972
|
+
}, collection, collectionPath, table, {
|
|
3973
|
+
operation,
|
|
3974
|
+
query
|
|
3975
|
+
}, this.filterContext(collectionPath, table));
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* The read the hooks are shown, from the options a read path was given.
|
|
3979
|
+
*
|
|
3980
|
+
* Transport is left out on purpose — a `databaseId`, an `include` tree, a
|
|
3981
|
+
* `withDeleted` flag are not part of "which rows". What is in is what a
|
|
3982
|
+
* scope might reasonably branch on.
|
|
3983
|
+
*/
|
|
3984
|
+
static describeRead(options) {
|
|
3985
|
+
const orderBy = normalizeDriverOrderBy(options.orderBy, options.order);
|
|
3986
|
+
return {
|
|
3987
|
+
filter: options.filter,
|
|
3988
|
+
logical: options.logical,
|
|
3989
|
+
searchString: options.searchString,
|
|
3990
|
+
limit: options.limit,
|
|
3991
|
+
offset: options.offset,
|
|
3992
|
+
orderBy: orderBy && orderBy.length > 0 ? orderBy : void 0,
|
|
3993
|
+
fields: options.fields,
|
|
3994
|
+
relatedTo: options.relatedTo ? {
|
|
3995
|
+
parentSlug: options.relatedTo.parentCollection.slug,
|
|
3996
|
+
parentId: options.relatedTo.parentId,
|
|
3997
|
+
relationName: options.relatedTo.relation.relationName
|
|
3998
|
+
} : void 0
|
|
3999
|
+
};
|
|
4000
|
+
}
|
|
4001
|
+
/**
|
|
3693
4002
|
* The table column this collection's rows are keyed by, or `undefined`.
|
|
3694
4003
|
*
|
|
3695
4004
|
* `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
|
|
@@ -4282,13 +4591,15 @@ var FetchService = class FetchService {
|
|
|
4282
4591
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
4283
4592
|
if (!idCanAddressTable(id, table, idInfoArray)) return void 0;
|
|
4284
4593
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
4594
|
+
const narrowing = await this.narrowReadCondition(collectionPath, collection, table, "get", {});
|
|
4595
|
+
const identity = narrowing ? and(eq(idField, parsedId), narrowing) : eq(idField, parsedId);
|
|
4285
4596
|
const tableName = getTableName(table);
|
|
4286
4597
|
const qb = this.getQueryBuilder(tableName);
|
|
4287
4598
|
if (qb) try {
|
|
4288
4599
|
const withConfig = this.buildWithConfig(collection);
|
|
4289
4600
|
const hidden = hiddenColumnsOption(getTableColumns(table), collection);
|
|
4290
4601
|
const row = await qb.findFirst({
|
|
4291
|
-
where: andSoftDelete(
|
|
4602
|
+
where: andSoftDelete(identity, collection, table, withDeleted),
|
|
4292
4603
|
with: withConfig,
|
|
4293
4604
|
...hidden ? { columns: hidden } : {}
|
|
4294
4605
|
});
|
|
@@ -4305,7 +4616,7 @@ var FetchService = class FetchService {
|
|
|
4305
4616
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
4306
4617
|
}
|
|
4307
4618
|
const visibleOne = visibleColumnProjection(getTableColumns(table), collection);
|
|
4308
|
-
const result = await this.db.select(visibleOne).from(table).where(andSoftDelete(
|
|
4619
|
+
const result = await this.db.select(visibleOne).from(table).where(andSoftDelete(identity, collection, table, withDeleted)).limit(1);
|
|
4309
4620
|
if (result.length === 0) return void 0;
|
|
4310
4621
|
const raw = result[0];
|
|
4311
4622
|
const values = await parseDataFromServer(raw, collection, this.db, this.registry);
|
|
@@ -4406,6 +4717,10 @@ var FetchService = class FetchService {
|
|
|
4406
4717
|
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, effectivePath, this.filterContext(effectivePath, table));
|
|
4407
4718
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
4408
4719
|
}
|
|
4720
|
+
allConditions.push(...await this.narrowRead(effectivePath, collection, table, "count", FetchService.describeRead({
|
|
4721
|
+
...options,
|
|
4722
|
+
relatedTo: hop
|
|
4723
|
+
})));
|
|
4409
4724
|
if (options.vectorSearch) {
|
|
4410
4725
|
const vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
4411
4726
|
if (vectorMeta.filter) allConditions.push(vectorMeta.filter);
|
|
@@ -4484,6 +4799,7 @@ var FetchService = class FetchService {
|
|
|
4484
4799
|
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
4485
4800
|
if (logicalCondition) conditions.push(logicalCondition);
|
|
4486
4801
|
}
|
|
4802
|
+
conditions.push(...await this.narrowRead(collectionPath, collection, table, "aggregate", FetchService.describeRead(options)));
|
|
4487
4803
|
if (conditions.length > 0) {
|
|
4488
4804
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(conditions);
|
|
4489
4805
|
if (finalCondition) query = query.where(finalCondition);
|
|
@@ -4505,7 +4821,15 @@ var FetchService = class FetchService {
|
|
|
4505
4821
|
});
|
|
4506
4822
|
}
|
|
4507
4823
|
/**
|
|
4508
|
-
* Check if a field value is unique
|
|
4824
|
+
* Check if a field value is unique.
|
|
4825
|
+
*
|
|
4826
|
+
* Deliberately **not** narrowed by `beforeQuery`, and it is the only read
|
|
4827
|
+
* here that is not. The question is whether the value exists anywhere in
|
|
4828
|
+
* the table, not whether it exists among the rows this caller can see:
|
|
4829
|
+
* narrowed, it would answer "unique" for a value a hidden row already
|
|
4830
|
+
* holds, and the insert would then be rejected by the unique constraint
|
|
4831
|
+
* with a message about a row the caller has no way to find. Uniqueness is a
|
|
4832
|
+
* property of the table.
|
|
4509
4833
|
*/
|
|
4510
4834
|
async checkUniqueField(collectionPath, fieldName, value, excludeEntityId, _databaseId) {
|
|
4511
4835
|
if (value === void 0 || value === null) return true;
|
|
@@ -4583,7 +4907,9 @@ var FetchService = class FetchService {
|
|
|
4583
4907
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
4584
4908
|
const withDeleted = options?.withDeleted;
|
|
4585
4909
|
const projection = this.columnProjection(table, collection, options?.fields, idInfoArray);
|
|
4586
|
-
const
|
|
4910
|
+
const narrowing = await this.narrowReadCondition(collectionPath, collection, table, "get", FetchService.describeRead({ fields: options?.fields }));
|
|
4911
|
+
const identity = narrowing ? and(eq(idField, parsedId), narrowing) : eq(idField, parsedId);
|
|
4912
|
+
const result = await this.db.select(projection).from(table).where(andSoftDelete(identity, collection, table, withDeleted)).limit(1);
|
|
4587
4913
|
if (result.length === 0) return null;
|
|
4588
4914
|
const row = toRestRow(result[0], collection, this.registry);
|
|
4589
4915
|
const normalizedInclude = normalizeInclude(include);
|
|
@@ -4634,6 +4960,7 @@ var FetchService = class FetchService {
|
|
|
4634
4960
|
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
4635
4961
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
4636
4962
|
}
|
|
4963
|
+
allConditions.push(...await this.narrowRead(collectionPath, collection, table, "list", FetchService.describeRead(options)));
|
|
4637
4964
|
if (vectorMeta?.filter) allConditions.push(vectorMeta.filter);
|
|
4638
4965
|
if (allConditions.length > 0) {
|
|
4639
4966
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
@@ -4738,11 +5065,13 @@ function serializePivot(pivot, payload, label) {
|
|
|
4738
5065
|
var RelationWriteService = class {
|
|
4739
5066
|
db;
|
|
4740
5067
|
registry;
|
|
5068
|
+
callContext;
|
|
4741
5069
|
reads;
|
|
4742
|
-
constructor(db, registry) {
|
|
5070
|
+
constructor(db, registry, callContext) {
|
|
4743
5071
|
this.db = db;
|
|
4744
5072
|
this.registry = registry;
|
|
4745
|
-
this.
|
|
5073
|
+
this.callContext = callContext;
|
|
5074
|
+
this.reads = new RelationService(db, registry, callContext);
|
|
4746
5075
|
}
|
|
4747
5076
|
/**
|
|
4748
5077
|
* Remove the junction row linking a parent to `targetId`, leaving the target
|
|
@@ -5109,17 +5438,19 @@ function compileFieldOps(table, fieldOps, context) {
|
|
|
5109
5438
|
var PersistService = class {
|
|
5110
5439
|
db;
|
|
5111
5440
|
registry;
|
|
5441
|
+
callContext;
|
|
5112
5442
|
/** Reads: whether a row is under a parent, the key a link joins on. */
|
|
5113
5443
|
relationService;
|
|
5114
5444
|
/** Writes: junction membership, foreign-key stamping, links. */
|
|
5115
5445
|
relationWrites;
|
|
5116
5446
|
fetchService;
|
|
5117
|
-
constructor(db, registry) {
|
|
5447
|
+
constructor(db, registry, callContext) {
|
|
5118
5448
|
this.db = db;
|
|
5119
5449
|
this.registry = registry;
|
|
5120
|
-
this.
|
|
5121
|
-
this.
|
|
5122
|
-
this.
|
|
5450
|
+
this.callContext = callContext;
|
|
5451
|
+
this.relationService = new RelationService(db, registry, callContext);
|
|
5452
|
+
this.relationWrites = new RelationWriteService(db, registry, callContext);
|
|
5453
|
+
this.fetchService = new FetchService(db, registry, callContext);
|
|
5123
5454
|
}
|
|
5124
5455
|
/**
|
|
5125
5456
|
* Set the columns of one many-to-many link, without touching the membership.
|
|
@@ -5413,13 +5744,15 @@ var PersistService = class {
|
|
|
5413
5744
|
var DataService = class {
|
|
5414
5745
|
db;
|
|
5415
5746
|
registry;
|
|
5747
|
+
callContext;
|
|
5416
5748
|
fetchService;
|
|
5417
5749
|
persistService;
|
|
5418
|
-
constructor(db, registry) {
|
|
5750
|
+
constructor(db, registry, callContext) {
|
|
5419
5751
|
this.db = db;
|
|
5420
5752
|
this.registry = registry;
|
|
5421
|
-
this.
|
|
5422
|
-
this.
|
|
5753
|
+
this.callContext = callContext;
|
|
5754
|
+
this.fetchService = new FetchService(db, registry, callContext);
|
|
5755
|
+
this.persistService = new PersistService(db, registry, callContext);
|
|
5423
5756
|
}
|
|
5424
5757
|
/**
|
|
5425
5758
|
* Fetch a single row by ID
|
|
@@ -6173,7 +6506,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6173
6506
|
this.db = db;
|
|
6174
6507
|
this.registry = registry;
|
|
6175
6508
|
this.poolManager = poolManager;
|
|
6176
|
-
this.dataService = new DataService(db, registry);
|
|
6509
|
+
this.dataService = new DataService(db, registry, () => this.buildCallContext());
|
|
6177
6510
|
this.realtimeService = realtimeService;
|
|
6178
6511
|
this.historyService = historyService;
|
|
6179
6512
|
this.user = user;
|
|
@@ -6803,7 +7136,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6803
7136
|
*/
|
|
6804
7137
|
bindToTransaction(tx) {
|
|
6805
7138
|
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
|
|
6806
|
-
txDriver.dataService = new DataService(tx, this.registry);
|
|
7139
|
+
txDriver.dataService = new DataService(tx, this.registry, () => txDriver.buildCallContext());
|
|
6807
7140
|
txDriver.client = this.client;
|
|
6808
7141
|
txDriver._deferNotifications = this._deferNotifications;
|
|
6809
7142
|
txDriver._pendingNotifications = this._pendingNotifications;
|
|
@@ -6825,7 +7158,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
6825
7158
|
*/
|
|
6826
7159
|
callContextWithin(tx, user) {
|
|
6827
7160
|
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, user, this.poolManager, this.historyService);
|
|
6828
|
-
txDriver.dataService = new DataService(tx, this.registry);
|
|
7161
|
+
txDriver.dataService = new DataService(tx, this.registry, () => txDriver.buildCallContext());
|
|
6829
7162
|
txDriver.client = this.client;
|
|
6830
7163
|
return txDriver.buildCallContext();
|
|
6831
7164
|
}
|
|
@@ -7411,7 +7744,8 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
7411
7744
|
if (!this.user?.uid) logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
|
|
7412
7745
|
if (!this.user?.roles) logger.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object", { detail: this.user });
|
|
7413
7746
|
await applyAuthContext(tx, authContextOf(this.user), this.delegate.rlsUserRole);
|
|
7414
|
-
|
|
7747
|
+
let hookContext;
|
|
7748
|
+
const txEntityService = new DataService(tx, this.delegate.registry, () => hookContext ??= this.delegate.callContextWithin(tx, this.user));
|
|
7415
7749
|
const txDelegate = new PostgresBackendDriver(tx, this.delegate.realtimeService, this.delegate.registry, this.user, this.delegate.poolManager, this.delegate.historyService);
|
|
7416
7750
|
txDelegate.dataService = txEntityService;
|
|
7417
7751
|
txDelegate._deferNotifications = true;
|
|
@@ -9566,7 +9900,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9566
9900
|
isAnonymous: activeAuth.isAnonymous === true,
|
|
9567
9901
|
claims: activeAuth.claims
|
|
9568
9902
|
}, this.rlsUserRole);
|
|
9569
|
-
|
|
9903
|
+
let hookContext;
|
|
9904
|
+
const fetchedEntities = await new DataService(tx, this.registry, () => hookContext ??= callbackContextWithin(this.driver, tx, activeAuth)).fetchCollectionForRest(notifyPath, {
|
|
9570
9905
|
filter: collectionRequest.filter,
|
|
9571
9906
|
logical: collectionRequest.logical,
|
|
9572
9907
|
orderBy: collectionRequest.orderBy,
|
|
@@ -9665,7 +10000,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9665
10000
|
isAnonymous: activeAuth.isAnonymous === true,
|
|
9666
10001
|
claims: activeAuth.claims
|
|
9667
10002
|
}, this.rlsUserRole);
|
|
9668
|
-
|
|
10003
|
+
let hookContext;
|
|
10004
|
+
return countOnce(new DataService(tx, this.registry, () => hookContext ??= callbackContextWithin(this.driver, tx, activeAuth)));
|
|
9669
10005
|
});
|
|
9670
10006
|
} else total = await countOnce(this.dataService);
|
|
9671
10007
|
} catch (error) {
|
|
@@ -9744,7 +10080,8 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
9744
10080
|
isAnonymous: activeAuth.isAnonymous === true,
|
|
9745
10081
|
claims: activeAuth.claims
|
|
9746
10082
|
}, this.rlsUserRole);
|
|
9747
|
-
let
|
|
10083
|
+
let hookContext;
|
|
10084
|
+
let processedEntity = await new DataService(tx, this.registry, () => hookContext ??= callbackContextWithin(this.driver, tx, activeAuth)).fetchOneForRest(notifyPath, id, void 0, collection?.databaseId) ?? void 0;
|
|
9748
10085
|
if (processedEntity) {
|
|
9749
10086
|
const registryCollection = this.registry.getCollectionByPath(notifyPath);
|
|
9750
10087
|
const resolvedCollection = collection ? {
|
|
@@ -11592,7 +11929,7 @@ function createBackupCron(config) {
|
|
|
11592
11929
|
enabled: config.enabled ?? true,
|
|
11593
11930
|
timeoutSeconds: 3600,
|
|
11594
11931
|
async handler({ log }) {
|
|
11595
|
-
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-
|
|
11932
|
+
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-CaMOS76G.js").then((n) => n.r);
|
|
11596
11933
|
const { destination } = config;
|
|
11597
11934
|
if (destination.kind !== "local" && !config.storage) throw new Error(`Backup destination is ${destination.kind} but no storage controller was provided. Pass the backend's configured StorageController to createBackupCron({ storage }).`);
|
|
11598
11935
|
log(`Starting backup of "${dbName}"…`);
|
|
@@ -14745,7 +15082,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
14745
15082
|
* handful of `CREATE OR REPLACE FUNCTION`.
|
|
14746
15083
|
*/
|
|
14747
15084
|
const provisionRlsRuntime = async (queryable) => {
|
|
14748
|
-
const { RLS_BOOTSTRAP_STATEMENTS } = await import("./rls-bootstrap-sql-
|
|
15085
|
+
const { RLS_BOOTSTRAP_STATEMENTS } = await import("./rls-bootstrap-sql-H3rCFi3F.js").then((n) => n.i);
|
|
14749
15086
|
await queryable.query("SELECT pg_advisory_lock(hashtext('rebase_auth_functions_init'))");
|
|
14750
15087
|
try {
|
|
14751
15088
|
for (const statement of RLS_BOOTSTRAP_STATEMENTS) await queryable.query(statement);
|
|
@@ -15139,7 +15476,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15139
15476
|
* proved it can bootstrap with.
|
|
15140
15477
|
*/
|
|
15141
15478
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
15142
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
15479
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CpAgy51F.js").then((n) => n.n);
|
|
15143
15480
|
const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
|
|
15144
15481
|
for (const failure of plan.failures) logger.warn(describeEnsureFailure(failure));
|
|
15145
15482
|
return { applied: plan.actions.length - plan.failures.length };
|
|
@@ -15173,7 +15510,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15173
15510
|
await provisionRlsRuntime(provisioningQueryable(driverResult));
|
|
15174
15511
|
},
|
|
15175
15512
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
15176
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
15513
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-Dzd-2S81.js");
|
|
15177
15514
|
const queryable = provisioningQueryable(driverResult);
|
|
15178
15515
|
const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
|
|
15179
15516
|
if (!rows[0]?.present) await provisionRlsRuntime(queryable);
|
|
@@ -15184,7 +15521,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
15184
15521
|
for (const u of outcome.unsecured.filter((u) => u.grantWithdrawn)) logger.error(`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. Its privileges have been revoked from ${REBASE_USER_ROLE}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
|
|
15185
15522
|
if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
|
|
15186
15523
|
try {
|
|
15187
|
-
const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-
|
|
15524
|
+
const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-H3rCFi3F.js").then((n) => n.i);
|
|
15188
15525
|
await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
|
|
15189
15526
|
info: (m) => logger.info(m),
|
|
15190
15527
|
warn: (m) => logger.warn(m)
|