@rebasepro/common 0.20.0 → 0.21.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/index.es.js CHANGED
@@ -1735,7 +1735,7 @@ function propertyClaimCastType(name, collection, resolveCollection, depth = 0) {
1735
1735
  return "numeric";
1736
1736
  }
1737
1737
  case "reference": return primaryKeyClaimCastType(resolveTargetCollection(prop.path, resolveCollection), resolveCollection, depth);
1738
- case "relation": return primaryKeyClaimCastType(resolveTargetCollection(relationTargetSlug(prop.relation), resolveCollection), resolveCollection, depth);
1738
+ case "relation": return primaryKeyClaimCastType(relationTarget(name, prop, collection, resolveCollection), resolveCollection, depth);
1739
1739
  default: return "text";
1740
1740
  }
1741
1741
  }
@@ -1822,9 +1822,65 @@ function schemaOf(collection) {
1822
1822
  function resolveColumnName(propName, collection) {
1823
1823
  const prop = collection?.properties?.[propName];
1824
1824
  if (prop && "columnName" in prop && typeof prop.columnName === "string") return quoteColumnIdentifier(prop.columnName);
1825
+ const relationColumn = belongsToColumn(propName, prop, collection);
1826
+ if (relationColumn) return quoteColumnIdentifier(relationColumn);
1825
1827
  return quoteColumnIdentifier(toSnakeCase(propName));
1826
1828
  }
1827
1829
  /**
1830
+ * The foreign key column a `relation` property addresses, when it has one.
1831
+ *
1832
+ * A `belongsTo` property is a *link*, and its column is the relation's
1833
+ * `localKey` — `org` addresses `org_id`. `toSnakeCase` cannot know that, so a
1834
+ * rule naming a relation (`ownerField: "org"`, a tenancy declaration over a
1835
+ * `belongsTo`) compiled to a comparison against a column called `org`, which
1836
+ * does not exist. `CREATE POLICY` then fails, and a table with RLS enabled and
1837
+ * no policy denies every row — the loudest possible symptom for the quietest
1838
+ * possible cause, since the rule reads exactly right.
1839
+ *
1840
+ * Only `belongsTo`. Every other kind puts its column on the target table, in a
1841
+ * junction row, or nowhere at all, and there is no column here to name.
1842
+ */
1843
+ function belongsToColumn(propName, prop, collection) {
1844
+ if (prop?.type !== "relation") return void 0;
1845
+ const relation = resolvePropertyRelation(propName, collection);
1846
+ return relation?.kind === "belongsTo" ? relation.localKey : void 0;
1847
+ }
1848
+ /**
1849
+ * The resolved relation a `relation` property stands for, by the only name
1850
+ * both declarations share: the property's own key.
1851
+ *
1852
+ * A property may carry the link inline (`relation: { kind, target }`) or name
1853
+ * an entry in the collection's `relations` array — and the second form is
1854
+ * matched by the property key, so there is nothing on the property to read.
1855
+ * `resolveCollectionRelations` normalises both under that key, which is why
1856
+ * this asks it rather than reading the property.
1857
+ */
1858
+ function resolvePropertyRelation(propName, collection) {
1859
+ if (!collection) return void 0;
1860
+ try {
1861
+ return findRelation(resolveCollectionRelations(collection), propName);
1862
+ } catch {
1863
+ return;
1864
+ }
1865
+ }
1866
+ /**
1867
+ * The collection a `relation` property points at.
1868
+ *
1869
+ * Preferred over the property's raw `target` because only the resolved
1870
+ * relation covers both declarations, and because a resolved `target()` hands
1871
+ * back the collection itself — no registry needed. The slug path stays as the
1872
+ * fallback for a `target` written as a plain string, which cannot become a
1873
+ * collection without one.
1874
+ */
1875
+ function relationTarget(propName, prop, collection, resolveCollection) {
1876
+ const relation = resolvePropertyRelation(propName, collection);
1877
+ if (relation) try {
1878
+ const target = relation.target();
1879
+ if (target) return target;
1880
+ } catch {}
1881
+ return resolveTargetCollection(relationTargetSlug(prop.relation), resolveCollection);
1882
+ }
1883
+ /**
1828
1884
  * Every PostgreSQL keyword that cannot stand as a bare column reference.
1829
1885
  * Appendix C's two reserved categories — plain "reserved", and "reserved (can
1830
1886
  * be function or type name)" — since neither may name a column unquoted.
@@ -2440,6 +2496,65 @@ function callbackRefusal(stage, path) {
2440
2496
  }
2441
2497
  });
2442
2498
  }
2499
+ /**
2500
+ * The collection a callback tier is about to be handed — or a refusal.
2501
+ *
2502
+ * Every callback props type declares `collection: CollectionConfig`,
2503
+ * non-optional, and the documented global-callback examples dereference it
2504
+ * (`if (collection.slug === "audit_log") return;`). The driver resolves that
2505
+ * value from the registry, which answers `undefined` for a path it does not
2506
+ * know, so the tiers used to receive it through a cast that quietly dropped the
2507
+ * `| undefined`. A global `beforeSave` reading `collection.slug` then threw a
2508
+ * `TypeError` that `toCallbackError` reported as a 400 `CALLBACK_REJECTED` —
2509
+ * the author's own rule blamed for a value the framework failed to supply.
2510
+ *
2511
+ * Skipping the tier instead is not available. `afterRead` is documented as the
2512
+ * place for "security-critical redaction (PII masking, row filtering) — no read
2513
+ * path bypasses it", and a tier that silently does not run on the paths the
2514
+ * registry cannot resolve is precisely such a bypass.
2515
+ *
2516
+ * So the contract is the third option: **a callback tier never runs without a
2517
+ * collection**, because a path that has none is refused before one can. That
2518
+ * costs nothing, because it is already true — every read and every write
2519
+ * reaches the database through `getCollectionByPath` in the driver's collection
2520
+ * helpers, which raises this same "not found" for the same paths. Asking here
2521
+ * only asks earlier, while the answer is still a 404 about the request instead
2522
+ * of a `TypeError` attributed to the application's hook.
2523
+ *
2524
+ * @param collection The collection the driver resolved, if it resolved one.
2525
+ * @param path The collection path, for the message and `details`.
2526
+ */
2527
+ function requireCallbackCollection(collection, path) {
2528
+ if (!collection) throw new RebaseApiError(`Collection not found: ${path}`, {
2529
+ status: 404,
2530
+ code: "NOT_FOUND",
2531
+ details: { path }
2532
+ });
2533
+ return collection;
2534
+ }
2535
+ /**
2536
+ * The client a callback reads as `context.client` — or a refusal naming why
2537
+ * there is none.
2538
+ *
2539
+ * A driver is constructed before the server client exists, and
2540
+ * `initializeRebaseBackend` hands it the client afterwards. So a driver can
2541
+ * run callbacks without one: constructed on its own, or missed by that
2542
+ * injection, which is how a second database's callbacks once ran with
2543
+ * `context.client === undefined` while the type said it was there.
2544
+ *
2545
+ * Called from a getter on the context rather than when the context is built,
2546
+ * because most callbacks never touch `client` and must not fail for its
2547
+ * absence. The one that does gets this sentence instead of "Cannot read
2548
+ * properties of undefined". A 500, not the 400 `toCallbackError` makes of a
2549
+ * plain throw: the callback is not at fault, the server's wiring is.
2550
+ */
2551
+ function requireCallbackClient(client) {
2552
+ if (!client) throw new RebaseApiError("`context.client` is not available to this callback: the driver running it was never given the server client. `initializeRebaseBackend` attaches it to every data source's driver at boot, so this driver was constructed outside it or missed. Queries do not need it — they go through `context.data`.", {
2553
+ status: 500,
2554
+ code: "INTERNAL_ERROR"
2555
+ });
2556
+ return client;
2557
+ }
2443
2558
  //#endregion
2444
2559
  //#region src/util/tenant.ts
2445
2560
  /**
@@ -2894,20 +3009,47 @@ function mergeJunctionPayload(into, incoming, table, collection) {
2894
3009
  * payload column anywhere is a second description that can disagree.
2895
3010
  */
2896
3011
  function getJunctionCollectionConfig(spec) {
3012
+ return buildJunctionCollectionConfig({
3013
+ table: spec.table,
3014
+ schema: spec.schema,
3015
+ keyColumns: spec.endpoints.map((endpoint) => endpoint.junctionColumn),
3016
+ properties: spec.properties
3017
+ });
3018
+ }
3019
+ /**
3020
+ * The same synthetic collection, reached from a resolved relation rather than
3021
+ * from a spec.
3022
+ *
3023
+ * The spec is built by walking every collection, which the schema planner does
3024
+ * once at boot and no request path can afford. A read or a write already holds
3025
+ * the relation, and a relation's `through` carries the table, both key columns
3026
+ * and the payload — everything the config is made of. One builder underneath
3027
+ * both, so the shape the planner emitted columns from is the shape the write
3028
+ * path validates a `_pivot` against and the read path strips it with.
3029
+ */
3030
+ function getJunctionConfigForRelation(through) {
3031
+ return buildJunctionCollectionConfig({
3032
+ table: through.table.includes(".") ? through.table.split(".").pop() : through.table,
3033
+ schema: "public",
3034
+ keyColumns: [through.sourceColumn, through.targetColumn],
3035
+ properties: through.properties
3036
+ });
3037
+ }
3038
+ function buildJunctionCollectionConfig(args) {
2897
3039
  const properties = {};
2898
- for (const endpoint of spec.endpoints) properties[endpoint.junctionColumn] = {
3040
+ for (const column of args.keyColumns) properties[column] = {
2899
3041
  type: "string",
2900
- columnName: endpoint.junctionColumn
3042
+ columnName: column
2901
3043
  };
2902
- for (const [key, property] of Object.entries(spec.properties)) {
3044
+ for (const [key, property] of Object.entries(args.properties)) {
2903
3045
  if (key === JUNCTION_PIVOT_KEY || key in properties) continue;
2904
3046
  properties[key] = property;
2905
3047
  }
2906
3048
  return {
2907
- slug: spec.table,
2908
- name: spec.table,
2909
- table: spec.table,
2910
- schema: spec.schema,
3049
+ slug: args.table,
3050
+ name: args.table,
3051
+ table: args.table,
3052
+ schema: args.schema,
2911
3053
  properties
2912
3054
  };
2913
3055
  }
@@ -3599,6 +3741,44 @@ async function revokeInternalTableAccess(execute, schema, options) {
3599
3741
  }
3600
3742
  }
3601
3743
  //#endregion
3744
+ //#region src/util/sql-rows.ts
3745
+ /**
3746
+ * Read the rows out of whatever a SQL driver actually returned.
3747
+ *
3748
+ * Two shapes reach this repository's stores through the same call, and which
3749
+ * one arrives is a property of the driver rather than of the query:
3750
+ * node-postgres hands back a `{ rows }` envelope, and the other paths — a bare
3751
+ * `executeSql`, Drizzle's `db.execute()` on some builds — hand back an array.
3752
+ * `@rebasepro/server`'s `SqlExec` type declares the array, so the envelope is
3753
+ * off-contract every time it turns up, and it still turns up.
3754
+ *
3755
+ * Reading one shape only is how a store silently sees nothing: the rate limiter
3756
+ * that did it counted every caller as being on their first request — a limiter
3757
+ * that never limits, with no error anywhere to say so.
3758
+ *
3759
+ * Written inline it came to `result as unknown as { rows?: T[] } | T[]`, five
3760
+ * times across three packages. That is the problem restated as an assertion: a
3761
+ * union the caller has to re-test at runtime anyway, with `unknown` in the
3762
+ * middle only because neither half overlaps what the signature promised. The
3763
+ * `Array.isArray` below is that same test done once, and it *narrows* — so
3764
+ * these branches are checked rather than claimed.
3765
+ *
3766
+ * The element type is the one claim that stays a claim: these are rows from
3767
+ * hand-written SQL, and nothing at this layer can check a column list.
3768
+ */
3769
+ function sqlRows(result) {
3770
+ if (Array.isArray(result)) return result;
3771
+ if (result !== null && typeof result === "object") {
3772
+ const rows = result.rows;
3773
+ if (Array.isArray(rows)) return rows;
3774
+ }
3775
+ return [];
3776
+ }
3777
+ /** The first row of {@link sqlRows}, or `undefined` when there were none. */
3778
+ function firstSqlRow(result) {
3779
+ return sqlRows(result)[0];
3780
+ }
3781
+ //#endregion
3602
3782
  //#region src/data/resolveDataSource.ts
3603
3783
  /**
3604
3784
  * Build a keyed registry from a list of {@link DataSourceDefinition}s.
@@ -6668,6 +6848,12 @@ function toFilterTuples(filterParam) {
6668
6848
  }
6669
6849
  //#endregion
6670
6850
  //#region src/table-classification.ts
6851
+ /**
6852
+ * Table Classification
6853
+ *
6854
+ * Shared constants and pure functions for classifying database tables.
6855
+ * Used by both the server-side PostgresBackendDriver and the Studio RLS editor.
6856
+ */
6671
6857
  /** Schemas that are always considered Rebase-internal. */
6672
6858
  var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
6673
6859
  /** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
@@ -6689,8 +6875,29 @@ var REBASE_INTERNAL_PREFIXES = [
6689
6875
  * **not** handled by this function. Use {@link detectJunctionTables} to obtain
6690
6876
  * the set of junction tables, then reclassify as needed.
6691
6877
  */
6878
+ /** The schema Rebase puts both its own tables and the tenant's collections in. */
6879
+ var REBASE_SCHEMA = "rebase";
6880
+ /**
6881
+ * Rebase's own tables, by unqualified name.
6882
+ *
6883
+ * Reused from `util/internal-tables`, which maintains it for the privilege
6884
+ * revoke — rather than a second list here, which would be a second thing to
6885
+ * keep true. It already excludes `users` for the reason that matters to this
6886
+ * function too: the auth user table is also a collection, with RLS and
6887
+ * policies, and hiding it would hide the one table almost every policy
6888
+ * references.
6889
+ *
6890
+ * That list is deliberately willing to claim common nouns — `jobs`, `branches`,
6891
+ * `api_keys` — because its own caller re-checks each against `relrowsecurity`.
6892
+ * This function has no such signal, so the layer above supplies it: a table the
6893
+ * project MAPS to a collection is classified as the customer's before this is
6894
+ * consulted at all.
6895
+ */
6896
+ var INTERNAL_TABLE_NAMES = new Set(REBASE_INTERNAL_TABLES);
6692
6897
  function classifyTable(tableName, schemaName) {
6693
- if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
6898
+ if (REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
6899
+ if (schemaName === REBASE_SCHEMA) return INTERNAL_TABLE_NAMES.has(tableName) ? "rebase-internal" : "user";
6900
+ if (REBASE_INTERNAL_SCHEMAS.includes(schemaName)) return "rebase-internal";
6694
6901
  return "user";
6695
6902
  }
6696
6903
  /**
@@ -6743,6 +6950,6 @@ async function detectJunctionTables(executeSql) {
6743
6950
  return junctionTables;
6744
6951
  }
6745
6952
  //#endregion
6746
- export { ADMIN_ROLE, CALLBACK_REJECTED, COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, CursorError, CursorMismatchError, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, IncludeSpecError, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, OrderBySpecError, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_TABLES, REBASE_USER_ROLE, RebasePaginationError, TENANT_INDEX_REASON, UnknownFilterOperatorError, aggregateAlias, and, applyDefaultValuesOnCreate, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, buildTenantSecurityRule, callbackRefusal, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, canReadField, canWriteField, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, cursorToStartAfter, decodeCursor, defaultUsersCollection, defineCollection, denormalizeInclude, deserializeFilter, deserializeInclude, deserializeLogicalCondition, deserializeOrderBy, deserializeOrderByList, detectJunctionTables, effectiveAccess, embedParentExpression, encodeCursor, enumToObjectEntries, evaluateCondition, evaluatePolicy, fieldKeyForColumn, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, getTenantConfig, hasFieldAccessRules, includePaths, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationRequired, isRelationalCollection, mergeIncludeSpecs, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeOrderBy, normalizeToEntityRelation, not, or, paginateFind, parseIdValues, parseOrderBySpecStrict, policyToPostgres, primaryOrderBy, reconcileCursorOrder, registerConditionOperations, relationDeclaringProperty, relationalCollections, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, resolveTenantWrite, restrictedFieldNames, revokeInternalTableAccess, revokeInternalTableSql, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy, sortCollectionsBySlug, sortProperties, sqlToPolicy, stripCollectionPath, tenantBypassRoles, tenantPolicyName, tenantScopeExpression, toCallbackError, toFilterTuples, topLevelIncludeNames, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, updateUserAutoValues, wrapAsEntityData, wrapAsSdkData };
6953
+ export { ADMIN_ROLE, CALLBACK_REJECTED, COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, CursorError, CursorMismatchError, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, IncludeSpecError, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, OrderBySpecError, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_TABLES, REBASE_USER_ROLE, RebasePaginationError, TENANT_INDEX_REASON, UnknownFilterOperatorError, aggregateAlias, and, applyDefaultValuesOnCreate, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, buildTenantSecurityRule, callbackRefusal, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, canReadField, canWriteField, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, cursorToStartAfter, decodeCursor, defaultUsersCollection, defineCollection, denormalizeInclude, deserializeFilter, deserializeInclude, deserializeLogicalCondition, deserializeOrderBy, deserializeOrderByList, detectJunctionTables, effectiveAccess, embedParentExpression, encodeCursor, enumToObjectEntries, evaluateCondition, evaluatePolicy, fieldKeyForColumn, findAnonymousGrants, findRelation, firstSqlRow, fullPathToCollectionSegments, getArrayResolvedProperties, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionConfigForRelation, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, getTenantConfig, hasFieldAccessRules, includePaths, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationRequired, isRelationalCollection, mergeIncludeSpecs, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeOrderBy, normalizeToEntityRelation, not, or, paginateFind, parseIdValues, parseOrderBySpecStrict, policyToPostgres, primaryOrderBy, reconcileCursorOrder, registerConditionOperations, relationDeclaringProperty, relationalCollections, requireCallbackClient, requireCallbackCollection, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, resolveTenantWrite, restrictedFieldNames, revokeInternalTableAccess, revokeInternalTableSql, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy, sortCollectionsBySlug, sortProperties, sqlRows, sqlToPolicy, stripCollectionPath, tenantBypassRoles, tenantPolicyName, tenantScopeExpression, toCallbackError, toFilterTuples, topLevelIncludeNames, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, updateUserAutoValues, wrapAsEntityData, wrapAsSdkData };
6747
6954
 
6748
6955
  //# sourceMappingURL=index.es.js.map