@rebasepro/common 0.13.0 → 0.13.1-canary.g06dbe5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/data/buildRebaseData.d.ts +10 -1
  2. package/dist/data/filter-conditions.d.ts +34 -0
  3. package/dist/data/filter-dialect.d.ts +19 -3
  4. package/dist/data/paginate.d.ts +20 -0
  5. package/dist/data/query_builder.d.ts +16 -4
  6. package/dist/data/resolveDataSource.d.ts +36 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.es.js +622 -92
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/util/builders.d.ts +2 -2
  11. package/dist/util/collections.d.ts +17 -0
  12. package/dist/util/conditions.d.ts +7 -3
  13. package/dist/util/entities.d.ts +8 -1
  14. package/dist/util/index.d.ts +1 -0
  15. package/dist/util/internal-tables.d.ts +95 -0
  16. package/dist/util/permissions.d.ts +30 -0
  17. package/dist/util/policy/sqlToPolicy.d.ts +4 -4
  18. package/dist/util/relations.d.ts +18 -1
  19. package/dist/util/resolutions.d.ts +31 -0
  20. package/package.json +4 -3
  21. package/src/data/buildRebaseData.ts +161 -27
  22. package/src/data/filter-conditions.ts +46 -0
  23. package/src/data/filter-dialect.ts +110 -37
  24. package/src/data/paginate.ts +30 -0
  25. package/src/data/query_builder.ts +26 -4
  26. package/src/data/resolveDataSource.ts +56 -0
  27. package/src/index.ts +1 -0
  28. package/src/util/auth-default-policies.ts +8 -2
  29. package/src/util/builders.ts +3 -3
  30. package/src/util/collections.ts +17 -1
  31. package/src/util/conditions.ts +8 -3
  32. package/src/util/entities.ts +15 -1
  33. package/src/util/index.ts +1 -0
  34. package/src/util/internal-tables.ts +154 -0
  35. package/src/util/permissions.test.ts +23 -2
  36. package/src/util/permissions.ts +43 -6
  37. package/src/util/policy/evaluatePolicy.ts +24 -2
  38. package/src/util/policy/policyToPostgres.ts +23 -10
  39. package/src/util/policy/sqlToPolicy.ts +127 -26
  40. package/src/util/relations.ts +31 -0
  41. package/src/util/resolutions.ts +77 -5
@@ -3,7 +3,7 @@ import { FirebaseCollectionConfig, FirebaseProperties, InferEntityType, MongoDBC
3
3
  * Define a PostgreSQL-backed collection with full type inference.
4
4
  *
5
5
  * The `const P` generic captures literal property types from your
6
- * `properties` object, which enables autocomplete on `titleProperty`,
6
+ * `properties` object, which enables autocomplete on `display.title`,
7
7
  * `sort`, `propertiesOrder`, `fixedFilter`, and entity callbacks.
8
8
  *
9
9
  * @example
@@ -16,7 +16,7 @@ import { FirebaseCollectionConfig, FirebaseProperties, InferEntityType, MongoDBC
16
16
  * name: { name: "Name", type: "string", validation: { required: true } },
17
17
  * price: { name: "Price", type: "number" },
18
18
  * },
19
- * titleProperty: "name", // ✅ autocomplete: "name" | "price"
19
+ * display: { title: "name" }, // ✅ autocomplete: "name" | "price"
20
20
  * sort: ["price", "asc"], // ✅ autocomplete on first element
21
21
  * });
22
22
  * ```
@@ -1,3 +1,20 @@
1
1
  import { CollectionConfig, Properties } from "@rebasepro/types";
2
2
  export declare function sortProperties<M extends Record<string, unknown>>(properties: Properties, propertiesOrder?: string[]): Properties;
3
+ /**
4
+ * A copy of `collections` ordered by slug.
5
+ *
6
+ * Every generator that turns collections into a file is order-dependent, and
7
+ * every one of them is compared against its own output — `rebase doctor`
8
+ * regenerates in memory and diffs, `generate-sdk && git diff --exit-code` gates
9
+ * CI. While only the *writers* sorted, a project whose `readdirSync` order
10
+ * differed from its slug order was reported permanently out of date, and the
11
+ * fix the message printed rewrote the file in the order it was already in. The
12
+ * generators sort themselves now, so no caller can get this wrong.
13
+ *
14
+ * A slug-less collection is left to the generator's own validation, which names
15
+ * the offending collection; sorting must not throw first.
16
+ */
17
+ export declare function sortCollectionsBySlug<C extends {
18
+ slug?: string;
19
+ }>(collections: readonly C[]): C[];
3
20
  export declare function getPrimaryKeys<M extends Record<string, unknown>>(collection: CollectionConfig<M>): Extract<keyof M, string>[];
@@ -1,13 +1,17 @@
1
- import { AuthState, ConditionContext, JsonLogicRule } from "@rebasepro/types";
1
+ import { AuthState, ConditionContext, ConditionRule } from "@rebasepro/types";
2
2
  /**
3
3
  * Register custom JSON Logic operations for Rebase.
4
4
  * Call this once at app initialization.
5
5
  */
6
6
  export declare function registerConditionOperations(): void;
7
7
  /**
8
- * Evaluate a JSON Logic rule against the given context.
8
+ * Evaluate a condition against the given context.
9
+ *
10
+ * A condition may be stated as a literal instead of a rule — `hidden: true`
11
+ * rather than `hidden: { "==": [1, 1] }` — and a literal is already its own
12
+ * answer, so it is returned rather than handed to the evaluator.
9
13
  */
10
- export declare function evaluateCondition(rule: JsonLogicRule, context: ConditionContext): unknown;
14
+ export declare function evaluateCondition(rule: ConditionRule, context: ConditionContext): unknown;
11
15
  /**
12
16
  * Build a ConditionContext from the current property resolution context.
13
17
  */
@@ -31,9 +31,16 @@ export declare function getRelationFrom<M extends Record<string, unknown>>(entit
31
31
  * have `id` and `path` fields — these are relation-shaped objects from
32
32
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
33
33
  *
34
+ * When `targetPath` is given, also accepts a bare id. A relation column is a
35
+ * foreign key, and the REST layer returns it as the scalar it is; only some
36
+ * fetch paths hydrate it into an object. Which form a caller sees therefore
37
+ * depends on how the row was loaded, and a caller that only accepted objects
38
+ * reported half of its own data as a type error. The declared target is the
39
+ * missing half: with it, an id is a relation that has not been fetched yet.
40
+ *
34
41
  * Returns null if the value cannot be coerced.
35
42
  */
36
- export declare function normalizeToEntityRelation(value: unknown, propertyType?: string): EntityRelation | null;
43
+ export declare function normalizeToEntityRelation(value: unknown, propertyType?: string, targetPath?: string): EntityRelation | null;
37
44
  export declare function traverseValuesProperties<M extends Record<string, unknown>>(inputValues: Partial<EntityValues<M>>, properties: Properties, operation: (value: unknown, property: Property) => unknown): EntityValues<M> | undefined;
38
45
  export declare function traverseValueProperty(inputValue: unknown, property: Property, operation: (value: unknown, property: Property) => unknown): unknown;
39
46
  /**
@@ -18,3 +18,4 @@ export * from "./junction-policies";
18
18
  export * from "./conditions";
19
19
  export * from "./pg-column-to-property";
20
20
  export * from "./string-column-length";
21
+ export * from "./internal-tables";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
3
+ * end-user role away from them.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
8
+ * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
9
+ * in the schemas a project uses — including `rebase`, because a project's own
10
+ * collections are allowed to live there (the scaffold puts `users` there). It
11
+ * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
12
+ * migrating role inherits the same grant.
13
+ *
14
+ * Every framework-internal table is created later: auth's tables come up during
15
+ * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
16
+ * job registers, `idempotency_keys` on the first request that carries a key. So
17
+ * they all inherited full DML for the end-user role — and none of them enables
18
+ * row-level security, because none of them is a collection with
19
+ * `securityRules`. Measured on a freshly provisioned database, `SET ROLE
20
+ * rebase_user` could read `rebase.refresh_tokens` (session token hashes),
21
+ * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
22
+ * `rebase.api_keys` (including its `admin` flag), and insert into
23
+ * `rebase.app_config`.
24
+ *
25
+ * Nothing routes a user-context query at those tables today, so this was not
26
+ * reachable over the API. That is the wrong thing to depend on: the documented
27
+ * model is that RLS is the authorization boundary, and these tables sat outside
28
+ * it. The boundary is now a privilege boundary instead — the role simply cannot
29
+ * address them.
30
+ *
31
+ * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
32
+ *
33
+ * RLS with no policy denies every row, which is the same outcome, but it is the
34
+ * *weaker* statement: it leaves the grant in place, so a later policy — or a
35
+ * `FORCE` flag cleared by some future migration — reopens the table. There is no
36
+ * row of `refresh_tokens` any end user should ever reach, so the honest encoding
37
+ * is "this role has no privilege here at all". It also keeps the owner
38
+ * connection (which auth actually runs on) completely unaffected.
39
+ *
40
+ * ## Keeping it true
41
+ *
42
+ * `packages/rls-check` scans the `rebase` schema — it used to skip it as a
43
+ * "platform" schema — and its `rls-disabled` check fires on exactly the
44
+ * condition this module removes: RLS off *and* a DML grant to a reachable role.
45
+ * So a table added here without a revoke is caught by `pnpm rls:check`, not by
46
+ * someone re-reading this file.
47
+ */
48
+ /**
49
+ * The Postgres role authenticated requests run as.
50
+ *
51
+ * Defined here rather than in the Postgres driver because both the driver (which
52
+ * provisions the role) and this module (which revokes on its behalf) need it,
53
+ * and a second spelling of a role name is a silent no-op waiting to happen.
54
+ */
55
+ export declare const REBASE_USER_ROLE = "rebase_user";
56
+ /**
57
+ * Framework-internal table names, unqualified.
58
+ *
59
+ * Deliberately NOT including `users`: the auth user table is also a collection,
60
+ * with `securityRules`, RLS enabled and policies applied. Users read their own
61
+ * row through it — revoking there would break sign-in.
62
+ *
63
+ * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
64
+ * because `db migrate apply` passes `--revisions-schema rebase`.
65
+ */
66
+ export declare const REBASE_INTERNAL_TABLES: readonly string[];
67
+ /**
68
+ * A single statement that takes every privilege on `schema.table` away from the
69
+ * end-user role.
70
+ *
71
+ * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
72
+ * happen in practice:
73
+ *
74
+ * - the role does not exist when the connection is unprivileged (Rebase then
75
+ * relies on native RLS rather than a role switch), and a bare `REVOKE` on a
76
+ * missing role is an error, not a no-op;
77
+ * - the table may not exist yet — `cron_logs` never appears in a project with
78
+ * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
79
+ *
80
+ * One command, so it is safe on handles that speak the extended query protocol
81
+ * and reject multi-statement strings.
82
+ */
83
+ export declare function revokeInternalTableSql(schema: string, table: string): string;
84
+ /**
85
+ * Revoke on every internal table in `schema`, one statement at a time.
86
+ *
87
+ * Best-effort per table: a connection that does not own one of them (a
88
+ * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
89
+ * that must not take down a boot. The caller decides how loud to be — `onError`
90
+ * exists so the driver can warn without this module importing a logger.
91
+ */
92
+ export declare function revokeInternalTableAccess(execute: (sql: string) => Promise<unknown>, schema: string, options?: {
93
+ tables?: readonly string[];
94
+ onError?: (table: string, error: unknown) => void;
95
+ }): Promise<void>;
@@ -21,8 +21,27 @@ export type AuthContext<USER extends User = User> = AuthState<USER>;
21
21
  * applying policies in-process), so an undecidable rule never silently allows.
22
22
  */
23
23
  export type UnknownResolution = "allow" | "deny";
24
+ /**
25
+ * Which half of a rule to evaluate.
26
+ *
27
+ * Postgres evaluates `USING` against the row as it is *now* and `WITH CHECK`
28
+ * against the row as it *will be*, both inside the transaction. A driver
29
+ * enforcing an update in-process has two different rows in hand and therefore
30
+ * needs to ask the two questions separately — asking one question about one row
31
+ * either checks the new values against the old row's ownership or the reverse.
32
+ *
33
+ * - `"both"` (default): what a single-row decision means (`USING ∧ WITH CHECK`).
34
+ * - `"using"`: the read/target clause only — ask it about the stored row.
35
+ * - `"withCheck"`: the write clause only — ask it about the row being written.
36
+ *
37
+ * Rule *selection* is unaffected: the target operation still decides which rules
38
+ * apply, so `"using"` on an `update` evaluates the update rules' USING clause,
39
+ * not the delete rules'.
40
+ */
41
+ export type PolicyClauses = "both" | "using" | "withCheck";
24
42
  export interface CheckOperationOptions {
25
43
  onUnknown?: UnknownResolution;
44
+ clauses?: PolicyClauses;
26
45
  }
27
46
  /**
28
47
  * Decide whether an operation is permitted for a user on a (possibly null) row,
@@ -30,9 +49,20 @@ export interface CheckOperationOptions {
30
49
  * the same model compiled to Postgres RLS DDL, so the decision matches database
31
50
  * enforcement for every non-raw rule.
32
51
  *
52
+ * Engine-independent by design. `securityRules` are a declaration about the
53
+ * data, not about Postgres: the engine decides *who* enforces them (Postgres
54
+ * compiles them to RLS DDL, a document driver applies them in-process), never
55
+ * *whether* they hold. Gating this function on the engine's `supportsRLS`
56
+ * capability is what made every `{ onUnknown: "deny" }` call site in the Mongo
57
+ * driver return `true` before it evaluated anything — and it did so only for
58
+ * collections that spelled their engine out, so declaring `engine: "mongodb"`
59
+ * was what switched authorization off.
60
+ *
33
61
  * @param options.onUnknown how to treat rules that cannot be decided
34
62
  * client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
35
63
  * for optimistic UI gating; enforcement callers should pass `"deny"`.
64
+ * @param options.clauses which half of each rule to evaluate. See
65
+ * {@link PolicyClauses}; defaults to `"both"`.
36
66
  */
37
67
  export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: CollectionConfig<M>, authContext: AuthContext<USER>, entity: Entity<M> | null, targetOperation: SecurityOperation, options?: CheckOperationOptions): boolean;
38
68
  export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: CollectionConfig<M>, authContext: AuthContext<USER>): boolean;
@@ -12,13 +12,13 @@ export interface AnonymousGrantRisk {
12
12
  /**
13
13
  * Find clauses that read as "signed-in users only" but admit anonymous callers.
14
14
  *
15
- * Both spellings come from the same place — Supabase, where `auth.uid()` really
16
- * is NULL for an anonymous request. Rebase substitutes
15
+ * Both spellings come from the same place — Supabase, where its own `auth.uid()`
16
+ * really is NULL for an anonymous request. Rebase substitutes
17
17
  * {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
18
18
  * is how the trusted *server* context is recognised), so:
19
19
  *
20
- * - `auth.uid() IS NOT NULL` is a tautology on the user path, and
21
- * - `auth.uid() != 'anon'` excludes one spelling of anonymous and admits the
20
+ * - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
21
+ * - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
22
22
  * other. This one is not hypothetical and was not only a foreign habit:
23
23
  * rebase's own request path reported `'anon'` while everything that compiled
24
24
  * or checked a policy used `'anonymous'`, so whichever literal an author
@@ -1,4 +1,4 @@
1
- import { CollectionConfig, ResolvedRelation } from "@rebasepro/types";
1
+ import { CollectionConfig, ResolvedRelation, RelationProperty } from "@rebasepro/types";
2
2
  /**
3
3
  * Whether the target rows are shared with other parents — a many-to-many, or a
4
4
  * multi-hop `via` chain.
@@ -27,6 +27,23 @@ export declare function isJunctionBackedRelation(relation: ResolvedRelation): bo
27
27
  * which is worth hearing about.
28
28
  */
29
29
  export declare function resolveCollectionRelations(collection: CollectionConfig): Record<string, ResolvedRelation>;
30
+ /**
31
+ * The path of the collection a relation property points at, derived from the
32
+ * property alone.
33
+ *
34
+ * A preview holds a property and a value and no collection, so it cannot call
35
+ * `resolveRelationProperty`. It does not need to: both forms that carry a
36
+ * target — the stamped `resolvedRelation` and the inline `relation` — name it
37
+ * directly. Only the third form, a relation declared by name in the
38
+ * collection's `relations` array, is out of reach, and that one has no target
39
+ * to read without the collection anyway.
40
+ *
41
+ * This is what lets a preview render a relation column that arrived as a bare
42
+ * foreign key: the id says *which* row, the declared target says *which
43
+ * collection*, and `RelationPreview` fetches the rest. Without it a scalar id
44
+ * is indistinguishable from a value of the wrong type.
45
+ */
46
+ export declare function getRelationTargetPath(property: RelationProperty): string | undefined;
30
47
  export declare function getTableName(collection: CollectionConfig): string;
31
48
  export declare function getTableVarName(tableName: string): string;
32
49
  export declare function getEnumVarName(tableName: string, propName: string): string;
@@ -92,6 +92,37 @@ export declare function resolveEnumValues(input: EnumValues): EnumValueConfig[]
92
92
  * 3. many-relations on an engine that has relations (SQL).
93
93
  */
94
94
  export declare function getEntityChildViews<M extends Record<string, unknown> = Record<string, unknown>>(collection: CollectionConfig<M>): EntityChildView[];
95
+ /**
96
+ * Each of `collection`'s tabs paired with the property that declared it, when a
97
+ * property declared it: child view key → property key.
98
+ *
99
+ * A many-relation can only be declared as a property — that is the documented
100
+ * and only mechanism — and {@link getEntityChildViews} promotes it to a tab. So
101
+ * one declaration reaches the panel twice, and neither surface knew about the
102
+ * other. The form rendered a relation picker beside the tab, and the collection
103
+ * table rendered *two* columns under one heading: the relation's own column,
104
+ * showing the child rows, and a jump-to-tab button carrying the same name.
105
+ *
106
+ * The pairing is what lets each surface decide which half is redundant, and it
107
+ * has to be a pairing rather than two sets because the two keys differ whenever
108
+ * a relation is named. The match is on the resolved `relationName` — the
109
+ * identity `getEntityChildViews` itself dedupes on — so a relation declared in
110
+ * `relations` and pointed at by a differently-named property is recognised too.
111
+ *
112
+ * A relation with no property of its own is absent here, which is the point: it
113
+ * has exactly one surface already, and nothing to weigh it against.
114
+ *
115
+ * Only top-level properties: a relation nested inside a `map` gets no tab.
116
+ */
117
+ export declare function getChildViewDeclaringProperties<M extends Record<string, unknown> = Record<string, unknown>>(collection: CollectionConfig<M>): Map<string, string>;
118
+ /**
119
+ * The property keys of `collection` whose relation is already one of its tabs.
120
+ *
121
+ * What a form asks: the tab is the treatment for a list of child rows, so the
122
+ * picker beside it is the redundant half. See
123
+ * {@link getChildViewDeclaringProperties}.
124
+ */
125
+ export declare function getChildViewRelationPropertyKeys<M extends Record<string, unknown> = Record<string, unknown>>(collection: CollectionConfig<M>): Set<string>;
95
126
  /**
96
127
  * The child views of `collection` as bare collections.
97
128
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/common",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.13.1-canary.g06dbe5b",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "fast-equals": "6.0.2",
42
42
  "json-logic-js": "^2.0.5",
43
- "@rebasepro/types": "0.13.0",
44
- "@rebasepro/utils": "0.13.0"
43
+ "@rebasepro/types": "0.13.1-canary.g06dbe5b",
44
+ "@rebasepro/utils": "0.13.1-canary.g06dbe5b"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@jest/globals": "^30.4.1",
@@ -53,6 +53,7 @@
53
53
  "@types/react-measure": "^2.0.12",
54
54
  "babel-plugin-react-compiler": "beta",
55
55
  "cross-env": "^10.1.0",
56
+ "fast-check": "^4.9.0",
56
57
  "jest": "^30.4.2",
57
58
  "ts-jest": "^29.4.12",
58
59
  "tsd": "^0.33.0",