@rebasepro/common 0.9.1-canary.ad25bc0 → 0.9.1-canary.b10dcdf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isPostgresCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
2
- import { deepClone, generateForeignKeyName, getIn, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
2
+ import { deepClone, generateForeignKeyName, getIn, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
3
3
  import jsonLogic from "json-logic-js";
4
4
  import { deepEqual } from "fast-equals";
5
5
  //#region src/util/common.ts
@@ -300,12 +300,17 @@ function parseIdValues(idValue, primaryKeys) {
300
300
  /**
301
301
  * The primary keys of a collection, as declared by its properties.
302
302
  *
303
- * The driver can also infer keys from the Drizzle schema, which the browser
304
- * cannot see — so the server normalizes what it resolved onto the config it
305
- * serves (see `stampPrimaryKeys`), and this reads that back. Returns an empty
306
- * array when a collection declares none, which callers must treat as "not
307
- * addressable" rather than defaulting to `id`: guessing a key that is not the
308
- * real one produces confidently wrong addresses.
303
+ * This is the only tier both sides can read, because it is the only one written
304
+ * in the config: the postgres driver can also infer keys from the Drizzle
305
+ * schema, which the browser never sees and is never sent — the admin compiles
306
+ * the collection files into its own bundle rather than being served them. A key
307
+ * that lives only in the Drizzle schema is therefore invisible here, and the
308
+ * server says so at boot (`warnOnKeysTheAdminCannotResolve`) naming the `isId`
309
+ * to add.
310
+ *
311
+ * Returns an empty array when a collection declares none, which callers must
312
+ * treat as "not addressable" rather than defaulting to `id`: guessing a key
313
+ * that is not the real one produces confidently wrong addresses.
309
314
  */
310
315
  function getDeclaredPrimaryKeys(collection) {
311
316
  const properties = collection.properties;
@@ -330,9 +335,15 @@ function getDeclaredPrimaryKeys(collection) {
330
335
  * The postgres driver tries, in order: properties marked `isId`; the primary
331
336
  * keys of the Drizzle schema; and finally a column literally named `id`. Only
332
337
  * the first and last are visible in a `CollectionConfig`, which is what both
333
- * sides share — so a collection whose key is *only* known to Drizzle, and is
334
- * not named `id`, resolves to nothing here. That is reported rather than
335
- * guessed: inventing a key produces addresses that look right and route wrong.
338
+ * sides share.
339
+ *
340
+ * So the two agree except on a collection that declares no `isId` and whose key
341
+ * is known only to Drizzle. There, the driver reads the real key, and this
342
+ * either resolves nothing (reported to the console by the caller) or — if the
343
+ * table happens to have an unrelated `id` property — resolves `id`, which is
344
+ * the wrong key and cannot be detected from here: the addresses look right and
345
+ * route wrong. Only the config can settle it, so the server names both cases
346
+ * at boot (`warnOnKeysTheAdminCannotResolve`) with the `isId` to add.
336
347
  */
337
348
  function resolvePrimaryKeys(collection) {
338
349
  const declared = getDeclaredPrimaryKeys(collection);
@@ -1791,7 +1802,7 @@ var buildPropertyCallbacks = (properties) => {
1791
1802
  * Opt out with `disableDefaultPolicies: true` to take full responsibility for
1792
1803
  * the collection's RLS.
1793
1804
  */
1794
- var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
1805
+ var SERVER_OR_ADMIN_EXPR$1 = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
1795
1806
  /** Write operations that must be admin-gated by default on auth collections. */
1796
1807
  var DEFAULT_GUARDED_OPS = [
1797
1808
  "insert",
@@ -1804,7 +1815,7 @@ function isAuthCollection(collection) {
1804
1815
  return auth === true || typeof auth === "object" && auth?.enabled === true;
1805
1816
  }
1806
1817
  /** The property marked as the row id (falls back to `id`). */
1807
- function getIdPropertyName(collection) {
1818
+ function getIdPropertyName$1(collection) {
1808
1819
  for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
1809
1820
  return "id";
1810
1821
  }
@@ -1824,26 +1835,26 @@ function getEffectiveSecurityRules(collection) {
1824
1835
  injected.push({
1825
1836
  name: `${tableName}_default_admin_read`,
1826
1837
  operations: ["select"],
1827
- condition: SERVER_OR_ADMIN_EXPR
1838
+ condition: SERVER_OR_ADMIN_EXPR$1
1828
1839
  });
1829
1840
  injected.push({
1830
1841
  name: `${tableName}_default_admin_write`,
1831
1842
  operations: [...DEFAULT_GUARDED_OPS],
1832
- condition: SERVER_OR_ADMIN_EXPR,
1833
- check: SERVER_OR_ADMIN_EXPR
1843
+ condition: SERVER_OR_ADMIN_EXPR$1,
1844
+ check: SERVER_OR_ADMIN_EXPR$1
1834
1845
  });
1835
1846
  if (isAuthCollection(collection)) {
1836
1847
  injected.push({
1837
1848
  name: `${tableName}_default_self_read`,
1838
1849
  operations: ["select"],
1839
- condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
1850
+ condition: policy.compare(policy.field(getIdPropertyName$1(collection)), "eq", policy.authUid())
1840
1851
  });
1841
1852
  injected.push({
1842
1853
  name: `${tableName}_require_admin_write`,
1843
1854
  mode: "restrictive",
1844
1855
  operations: [...DEFAULT_GUARDED_OPS],
1845
- condition: SERVER_OR_ADMIN_EXPR,
1846
- check: SERVER_OR_ADMIN_EXPR
1856
+ condition: SERVER_OR_ADMIN_EXPR$1,
1857
+ check: SERVER_OR_ADMIN_EXPR$1
1847
1858
  });
1848
1859
  }
1849
1860
  return [...explicit, ...injected];
@@ -1865,6 +1876,208 @@ function getInjectedSecurityRules(collection) {
1865
1876
  return getEffectiveSecurityRules(collection).slice(explicitCount);
1866
1877
  }
1867
1878
  //#endregion
1879
+ //#region src/util/junction-policies.ts
1880
+ var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
1881
+ /**
1882
+ * Walk every collection's resolved relations and aggregate the junction tables
1883
+ * they declare. Two collections may declare the same junction from opposite
1884
+ * sides (posts→tags and tags→posts through `posts_tags`); both become
1885
+ * `declaringSides` of one spec, so derived write grants consider both.
1886
+ */
1887
+ function resolveJunctionSpecs(collections) {
1888
+ const specs = /* @__PURE__ */ new Map();
1889
+ for (const collection of collections) {
1890
+ const resolved = resolveCollectionRelations(collection);
1891
+ for (const relation of Object.values(resolved)) {
1892
+ if (!relation.through) continue;
1893
+ const targetCollection = typeof relation.target === "function" ? relation.target() : void 0;
1894
+ if (!targetCollection) continue;
1895
+ const rawName = relation.through.table;
1896
+ const table = rawName.includes(".") ? rawName.split(".").pop() : rawName;
1897
+ const schema = "public";
1898
+ const source = {
1899
+ collection,
1900
+ junctionColumn: relation.through.sourceColumn,
1901
+ relation
1902
+ };
1903
+ const target = {
1904
+ collection: targetCollection,
1905
+ junctionColumn: relation.through.targetColumn
1906
+ };
1907
+ const existing = specs.get(table);
1908
+ if (!existing) specs.set(table, {
1909
+ table,
1910
+ schema,
1911
+ endpoints: [source, target],
1912
+ declaringSides: [source]
1913
+ });
1914
+ else if (!existing.declaringSides.some((s) => s.collection === collection)) existing.declaringSides.push(source);
1915
+ }
1916
+ }
1917
+ return specs;
1918
+ }
1919
+ /**
1920
+ * A synthetic CollectionConfig standing in for the junction during policy
1921
+ * compilation and naming. Its two FK columns carry explicit `columnName`s so
1922
+ * `outerField` operands resolve to the exact columns the CREATE TABLE emitted,
1923
+ * whatever their casing.
1924
+ */
1925
+ function getJunctionCollectionConfig(spec) {
1926
+ const properties = {};
1927
+ for (const endpoint of spec.endpoints) properties[endpoint.junctionColumn] = {
1928
+ type: "string",
1929
+ columnName: endpoint.junctionColumn
1930
+ };
1931
+ return {
1932
+ slug: spec.table,
1933
+ name: spec.table,
1934
+ table: spec.table,
1935
+ schema: spec.schema,
1936
+ properties
1937
+ };
1938
+ }
1939
+ /** The property marked as the row id (falls back to `id`). */
1940
+ function getIdPropertyName(collection) {
1941
+ for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
1942
+ return "id";
1943
+ }
1944
+ /** `EXISTS (SELECT 1 FROM endpoint WHERE endpoint.pk = junction.fk [AND extra])`. */
1945
+ function existsEndpoint(endpoint, extra) {
1946
+ const correlation = policy.compare(policy.field(getIdPropertyName(endpoint.collection)), "eq", policy.outerField(endpoint.junctionColumn));
1947
+ return policy.existsIn({
1948
+ collection: endpoint.collection.slug,
1949
+ where: extra ? policy.and(correlation, extra) : correlation
1950
+ });
1951
+ }
1952
+ /**
1953
+ * Whether a parent-rule expression keeps its meaning when moved inside the
1954
+ * junction's `EXISTS` subquery — and the re-scoped copy if it does.
1955
+ *
1956
+ * Returns `null` when the rule cannot be embedded faithfully: `raw` SQL
1957
+ * anywhere (its `{column}` placeholders would bind to the junction), or an
1958
+ * `outerField` inside a nested `existsIn` (it would bind to the junction while
1959
+ * the author meant the parent, and no operand can express "the middle scope").
1960
+ * Top-level `outerField`s are rewritten to `field`, which is what they meant.
1961
+ */
1962
+ function embedParentExpression(expr, depth = 0) {
1963
+ switch (expr.kind) {
1964
+ case "raw": return null;
1965
+ case "and":
1966
+ case "or": {
1967
+ const parts = [];
1968
+ for (const child of expr.operands) {
1969
+ const embedded = embedParentExpression(child, depth);
1970
+ if (!embedded) return null;
1971
+ parts.push(embedded);
1972
+ }
1973
+ return expr.kind === "and" ? policy.and(...parts) : policy.or(...parts);
1974
+ }
1975
+ case "not": {
1976
+ const embedded = embedParentExpression(expr.operand, depth);
1977
+ return embedded ? policy.not(embedded) : null;
1978
+ }
1979
+ case "existsIn": {
1980
+ const where = embedParentExpression(expr.where, depth + 1);
1981
+ return where ? policy.existsIn({
1982
+ collection: expr.collection,
1983
+ where
1984
+ }) : null;
1985
+ }
1986
+ case "compare": {
1987
+ const left = embedOperand(expr.left, depth);
1988
+ const right = embedOperand(expr.right, depth);
1989
+ if (!left || !right) return null;
1990
+ return {
1991
+ ...expr,
1992
+ left,
1993
+ right
1994
+ };
1995
+ }
1996
+ default: return expr;
1997
+ }
1998
+ }
1999
+ /** Re-scope an operand, or return `null` if its binding cannot be preserved. */
2000
+ function embedOperand(operand, depth) {
2001
+ if (operand.kind === "outerField") {
2002
+ if (depth === 0) return policy.field(operand.name);
2003
+ return null;
2004
+ }
2005
+ return operand;
2006
+ }
2007
+ /** Does the rule cover the `update` operation? */
2008
+ function coversUpdate(rule) {
2009
+ return getPolicyOperations(rule).some((op) => op === "update" || op === "all");
2010
+ }
2011
+ /**
2012
+ * The full derived policy set for a junction table: the locked server/admin
2013
+ * baseline, the endpoint-visibility read grant, inherited write grants, and
2014
+ * inherited restrictive gates. Returns `[]` when every declaring collection set
2015
+ * `disableDefaultPolicies` — the junction is then the author's to police, and
2016
+ * stays locked (RLS is still enabled) until they write policies for it.
2017
+ */
2018
+ function getJunctionSecurityRules(spec) {
2019
+ if (spec.declaringSides.every((side) => side.collection.disableDefaultPolicies)) return [];
2020
+ const rules = [];
2021
+ rules.push({
2022
+ name: `${spec.table}_default_admin_read`,
2023
+ operations: ["select"],
2024
+ condition: SERVER_OR_ADMIN_EXPR
2025
+ });
2026
+ rules.push({
2027
+ name: `${spec.table}_default_admin_write`,
2028
+ operations: [
2029
+ "insert",
2030
+ "update",
2031
+ "delete"
2032
+ ],
2033
+ condition: SERVER_OR_ADMIN_EXPR,
2034
+ check: SERVER_OR_ADMIN_EXPR
2035
+ });
2036
+ rules.push({
2037
+ name: `${spec.table}_default_edge_read`,
2038
+ operations: ["select"],
2039
+ condition: policy.and(existsEndpoint(spec.endpoints[0]), existsEndpoint(spec.endpoints[1]))
2040
+ });
2041
+ const writeGrants = [];
2042
+ for (const side of spec.declaringSides) {
2043
+ const updateRules = ((isPostgresCollectionConfig(side.collection) ? side.collection.securityRules : void 0) ?? []).filter(coversUpdate);
2044
+ const permissive = updateRules.filter((r) => r.mode !== "restrictive");
2045
+ const restrictive = updateRules.filter((r) => r.mode === "restrictive");
2046
+ const embeddedGates = [];
2047
+ let gatesEmbeddable = true;
2048
+ for (const gate of restrictive) {
2049
+ const using = securityRuleToConditions(gate).usingExpr;
2050
+ const embedded = using ? embedParentExpression(using) : null;
2051
+ if (!embedded) {
2052
+ gatesEmbeddable = false;
2053
+ break;
2054
+ }
2055
+ embeddedGates.push(embedded);
2056
+ }
2057
+ if (!gatesEmbeddable) continue;
2058
+ const grants = [];
2059
+ for (const rule of permissive) {
2060
+ const using = securityRuleToConditions(rule).usingExpr;
2061
+ const embedded = using ? embedParentExpression(using) : null;
2062
+ if (embedded) grants.push(embedded);
2063
+ }
2064
+ if (grants.length === 0) continue;
2065
+ const condition = embeddedGates.length > 0 ? policy.and(policy.or(...grants), ...embeddedGates) : policy.or(...grants);
2066
+ writeGrants.push(existsEndpoint(side, condition));
2067
+ }
2068
+ if (writeGrants.length > 0) rules.push({
2069
+ name: `${spec.table}_default_edge_write`,
2070
+ operations: [
2071
+ "insert",
2072
+ "update",
2073
+ "delete"
2074
+ ],
2075
+ condition: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants),
2076
+ check: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants)
2077
+ });
2078
+ return rules;
2079
+ }
2080
+ //#endregion
1868
2081
  //#region src/util/conditions.ts
1869
2082
  /**
1870
2083
  * Access a nested property from an object via dot notation.
@@ -3012,7 +3225,7 @@ function createPrimaryKeyResolver(options) {
3012
3225
  }
3013
3226
  if (!warned.has(slug)) {
3014
3227
  warned.add(slug);
3015
- console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config.`);
3228
+ console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config — the server logs which column to mark at boot, if its schema knows the key.`);
3016
3229
  }
3017
3230
  return keys;
3018
3231
  };
@@ -3610,6 +3823,6 @@ async function detectJunctionTables(executeSql) {
3610
3823
  return junctionTables;
3611
3824
  }
3612
3825
  //#endregion
3613
- export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, addInitialSlash, and, applyPropertyConditions, buildCollection, buildCompositeId, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityImagePreviewPropertyKey, getEnumVarName, getInjectedSecurityRules, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, isRebaseInternalTable, normalizeToEntityRelation, or, parseIdValues, policyToPostgres, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveFilterOperators, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
3826
+ export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, addInitialSlash, and, applyPropertyConditions, buildCollection, buildCompositeId, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityImagePreviewPropertyKey, getEnumVarName, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, isRebaseInternalTable, normalizeToEntityRelation, or, parseIdValues, policyToPostgres, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveFilterOperators, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, sanitizeRelation, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
3614
3827
 
3615
3828
  //# sourceMappingURL=index.es.js.map