@rebasepro/common 0.19.1 → 0.19.2-canary.g09316f6
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/collections/field-access.d.ts +76 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/data/buildRebaseData.d.ts +11 -12
- package/dist/data/cursor.d.ts +102 -0
- package/dist/data/include-spec.d.ts +128 -0
- package/dist/data/paginate.d.ts +8 -5
- package/dist/data/query_builder.d.ts +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +1415 -110
- package/dist/index.es.js.map +1 -1
- package/dist/util/entities.d.ts +79 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/junction-policies.d.ts +17 -1
- package/dist/util/tenant.d.ts +146 -0
- package/package.json +3 -3
package/dist/util/entities.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { DataType, Entity, EntityReference, EntityRelation, EntityStatus, EntityValues, Properties, Property } from "@rebasepro/types";
|
|
2
2
|
export declare function isPropertyBuilder(property?: Property): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* What a form opens with: a value for every property it can write.
|
|
5
|
+
*
|
|
6
|
+
* `excludeFromApi` columns are left out, and that is the whole of the rule —
|
|
7
|
+
* they are not part of the API surface in either direction, so there is nothing
|
|
8
|
+
* for a form to open showing and nothing it may send back. Including them was
|
|
9
|
+
* not cosmetic: the baseline is what gets submitted, so a new record carried
|
|
10
|
+
* `passwordHash: null` and `emailVerificationToken: null` into the create, and
|
|
11
|
+
* the server refused the whole write with "these columns are the server's to
|
|
12
|
+
* set" — the users collection could not be added to from the panel at all. The
|
|
13
|
+
* fields were invisible on screen (`admin.disabled.hidden`), which is what made
|
|
14
|
+
* the error read as being about the roles the operator *had* just edited.
|
|
15
|
+
*
|
|
16
|
+
* Server-side defaulting does not come through here: `applyDefaultValuesOnCreate`
|
|
17
|
+
* asks each property for its own default, so an excluded column with a declared
|
|
18
|
+
* `defaultValue` is still filled in on an in-process write.
|
|
19
|
+
*/
|
|
3
20
|
export declare function getDefaultValuesFor<M extends Record<string, unknown>>(properties: Properties): Partial<EntityValues<M>>;
|
|
4
21
|
export declare function getDefaultValueFor(property?: Property): unknown;
|
|
5
22
|
export declare function getDefaultValueFortype(type: DataType): unknown;
|
|
@@ -13,6 +30,68 @@ export declare function updateDateAutoValues<M extends Record<string, unknown>>(
|
|
|
13
30
|
status: EntityStatus;
|
|
14
31
|
timestampNowValue: unknown;
|
|
15
32
|
}): EntityValues<M>;
|
|
33
|
+
/**
|
|
34
|
+
* Stamp the acting user's uid into the `user_on_create` / `user_on_update`
|
|
35
|
+
* columns a collection declares.
|
|
36
|
+
*
|
|
37
|
+
* A deliberate sibling of {@link updateDateAutoValues} rather than another
|
|
38
|
+
* branch inside it. The two share a shape and nothing else: one takes an
|
|
39
|
+
* instant the server generates and the other takes an identity the request
|
|
40
|
+
* carries, so overloading the timestamp function would have meant threading a
|
|
41
|
+
* second, unrelated argument through every one of its callers and letting a
|
|
42
|
+
* `date` property and a `string` property compete for the same `autoValue`
|
|
43
|
+
* union. Called side by side in the driver.
|
|
44
|
+
*
|
|
45
|
+
* The stamped value overwrites whatever arrived in the body. A caller who can
|
|
46
|
+
* set `createdBy` is a caller who can attribute their write to somebody else,
|
|
47
|
+
* which is the one thing an audit column must not allow.
|
|
48
|
+
*
|
|
49
|
+
* `uid` is `undefined` for an anonymous request, a service token or an
|
|
50
|
+
* in-process write; the column is set to an explicit `null` there. Explicit
|
|
51
|
+
* matters on an update: leaving the key absent would keep whatever uid the
|
|
52
|
+
* column already held, so an anonymous edit would be recorded as the previous
|
|
53
|
+
* editor's. Refusing that write outright is `required`'s job, not this
|
|
54
|
+
* function's — see `assertWriteValuesValid`.
|
|
55
|
+
*
|
|
56
|
+
* Top-level properties only, deliberately, unlike {@link updateDateAutoValues}.
|
|
57
|
+
* `traverseValuesProperties` cannot express "set this key to null" — a `null`
|
|
58
|
+
* from its operation means "leave the key out" — and an audit column nested
|
|
59
|
+
* inside a `map` is not a column at all, so there is nothing down there to
|
|
60
|
+
* stamp.
|
|
61
|
+
*
|
|
62
|
+
* @group Driver
|
|
63
|
+
*/
|
|
64
|
+
export declare function updateUserAutoValues<M extends Record<string, unknown>>({ inputValues, properties, status, uid }: {
|
|
65
|
+
inputValues: Partial<EntityValues<M>>;
|
|
66
|
+
properties: Properties;
|
|
67
|
+
status: EntityStatus;
|
|
68
|
+
uid: string | undefined;
|
|
69
|
+
}): EntityValues<M>;
|
|
70
|
+
/**
|
|
71
|
+
* Fill in the `defaultValue`s a create left unset.
|
|
72
|
+
*
|
|
73
|
+
* `defaultValue` was read by exactly one thing: the Studio's form, which uses it
|
|
74
|
+
* to prefill inputs. Every other way into the same collection — the REST create,
|
|
75
|
+
* the SDK, the socket, an import — stored whatever arrived and nothing where the
|
|
76
|
+
* key was absent. So `active: { type: "boolean", defaultValue: true }` produced
|
|
77
|
+
* rows with `active` unset through the API and `true` through the panel, from
|
|
78
|
+
* one declaration that reads like a promise about the data.
|
|
79
|
+
*
|
|
80
|
+
* Only genuinely absent keys are filled. An explicit `null` is a caller saying
|
|
81
|
+
* "no value", which is a different statement from not mentioning the field, and
|
|
82
|
+
* overwriting it would make the default impossible to opt out of.
|
|
83
|
+
*
|
|
84
|
+
* `getDefaultValuesFor` also invents a per-type default for properties with no
|
|
85
|
+
* `defaultValue` at all (`false` for a boolean, `[]` for an array, `null` for
|
|
86
|
+
* the rest) — right for a form, which must render *something* in every input,
|
|
87
|
+
* and wrong here, where an absent key must stay absent so the column's own
|
|
88
|
+
* DEFAULT applies. Only declared defaults are taken.
|
|
89
|
+
*
|
|
90
|
+
* @param values the caller's payload
|
|
91
|
+
* @param properties the collection's declared properties
|
|
92
|
+
* @group Driver
|
|
93
|
+
*/
|
|
94
|
+
export declare function applyDefaultValuesOnCreate<M extends Record<string, unknown>>(values: Partial<EntityValues<M>> | undefined, properties: Properties): Partial<EntityValues<M>>;
|
|
16
95
|
/**
|
|
17
96
|
* Add missing required fields, expected in the collection, to the values of a entity
|
|
18
97
|
* @param values
|
package/dist/util/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./relations.js";
|
|
|
16
16
|
export * from "./resolve-relation.js";
|
|
17
17
|
export * from "./auth-default-policies.js";
|
|
18
18
|
export * from "./junction-policies.js";
|
|
19
|
+
export * from "./tenant.js";
|
|
19
20
|
export * from "./conditions.js";
|
|
20
21
|
export * from "./pg-column-to-property.js";
|
|
21
22
|
export * from "./string-column-length.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CollectionConfig, PolicyExpression, Relation, SecurityRule } from "@rebasepro/types";
|
|
1
|
+
import { CollectionConfig, PolicyExpression, Properties, Relation, SecurityRule } from "@rebasepro/types";
|
|
2
2
|
/**
|
|
3
3
|
* RLS derivation for many-to-many junction tables.
|
|
4
4
|
*
|
|
@@ -72,6 +72,15 @@ export interface JunctionSpec {
|
|
|
72
72
|
endpoints: [JunctionEndpoint, JunctionEndpoint];
|
|
73
73
|
/** Every collection that declares a relation through this table. */
|
|
74
74
|
declaringSides: JunctionDeclaringSide[];
|
|
75
|
+
/**
|
|
76
|
+
* The junction's own columns beyond the two keys — `through.properties`,
|
|
77
|
+
* merged across every declaring side. `{}` when there are none.
|
|
78
|
+
*
|
|
79
|
+
* See {@link ManyToManyRelation.through} for what they are; every side that
|
|
80
|
+
* names a key has to describe the same column, which is checked when the
|
|
81
|
+
* specs are resolved rather than left for `CREATE TABLE` to discover.
|
|
82
|
+
*/
|
|
83
|
+
properties: Properties;
|
|
75
84
|
}
|
|
76
85
|
/**
|
|
77
86
|
* Walk every collection's resolved relations and aggregate the junction tables
|
|
@@ -85,6 +94,13 @@ export declare function resolveJunctionSpecs(collections: CollectionConfig[]): M
|
|
|
85
94
|
* compilation and naming. Its two FK columns carry explicit `columnName`s so
|
|
86
95
|
* `outerField` operands resolve to the exact columns the CREATE TABLE emitted,
|
|
87
96
|
* whatever their casing.
|
|
97
|
+
*
|
|
98
|
+
* The payload columns are here too, exactly as authored. That is what lets one
|
|
99
|
+
* reading of a `Property` serve the junction as well as a collection: the
|
|
100
|
+
* schema planner plans these columns with the same function it plans a
|
|
101
|
+
* collection's with, and the write path validates a `_pivot` against them with
|
|
102
|
+
* the same validator a row's values go through. A second description of a
|
|
103
|
+
* payload column anywhere is a second description that can disagree.
|
|
88
104
|
*/
|
|
89
105
|
export declare function getJunctionCollectionConfig(spec: JunctionSpec): CollectionConfig;
|
|
90
106
|
/**
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one reading of `collection.tenant`.
|
|
3
|
+
*
|
|
4
|
+
* Four things have to agree for a tenant-scoped collection to work — the
|
|
5
|
+
* column, the RLS policy, the value stamped on insert and the index — and
|
|
6
|
+
* before this they were four hand-written declarations that nothing compared.
|
|
7
|
+
* The three that are schema become a {@link SecurityRule} and a column effect
|
|
8
|
+
* derived here and in `planSchema`; the fourth, the write path, is
|
|
9
|
+
* {@link resolveTenantWrite}.
|
|
10
|
+
*
|
|
11
|
+
* Everything in this module is pure. The policy it builds is a `SecurityRule`
|
|
12
|
+
* like any other, which is what makes `db push`, the doctor, boot-ensure, the
|
|
13
|
+
* drift detector and the Studio treat the tenancy policy as what it is —
|
|
14
|
+
* generated, named, and recognisable — rather than as somebody's hand-written
|
|
15
|
+
* SQL that a push should offer to drop.
|
|
16
|
+
*/
|
|
17
|
+
import { type CollectionConfig, type CollectionTenantConfig, type EntityStatus, type PolicyExpression, type SecurityRule } from "@rebasepro/types";
|
|
18
|
+
/**
|
|
19
|
+
* The collection's tenancy declaration, or nothing.
|
|
20
|
+
*
|
|
21
|
+
* Read through this rather than off the object, so the one shape check —
|
|
22
|
+
* `tenant` is an object carrying a `field` and a `from` — is in one place. A
|
|
23
|
+
* config that is *wrong* is refused by `validateCollectionConfig` with a
|
|
24
|
+
* message; this is only asking whether there is one.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getTenantConfig(collection: CollectionConfig | undefined): CollectionTenantConfig | undefined;
|
|
27
|
+
/** The roles tenancy does not apply to, defaulted. */
|
|
28
|
+
export declare function tenantBypassRoles(tenant: CollectionTenantConfig): readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* The name of the policy a tenant declaration compiles to.
|
|
31
|
+
*
|
|
32
|
+
* Explicit — not a `getPolicyNameHash` of the rule — precisely because the
|
|
33
|
+
* rule's *body* is compiled with more information in some callers than in
|
|
34
|
+
* others (`planSchema` can resolve a relation's target collection and so knows
|
|
35
|
+
* the column's type; the Studio, asking only for names, cannot). A hashed name
|
|
36
|
+
* would then differ between the two, and the same policy would read as drift.
|
|
37
|
+
* A frozen identifier: see `contracts/derived-names.txt`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function tenantPolicyName(tableName: string): string;
|
|
40
|
+
/** The `reason` on the index a tenant column gets. Rendered into `schema.sql`. */
|
|
41
|
+
export declare const TENANT_INDEX_REASON = "tenant scope";
|
|
42
|
+
/**
|
|
43
|
+
* The condition a tenant declaration means, as a policy expression.
|
|
44
|
+
*
|
|
45
|
+
* `serverContext()` first, for the same reason every injected baseline rule
|
|
46
|
+
* carries it: the trusted plane runs migrations, the auth flows and the boot,
|
|
47
|
+
* and a restrictive policy that excluded it would not protect a tenant, it
|
|
48
|
+
* would stop the server from starting.
|
|
49
|
+
*
|
|
50
|
+
* Then the bypass roles, then the tenancy test itself — a claim comparison or a
|
|
51
|
+
* correlated `EXISTS` over the membership table, which are the two ways a
|
|
52
|
+
* deployment answers "which tenant is this caller in".
|
|
53
|
+
*/
|
|
54
|
+
export declare function tenantScopeExpression(tenant: CollectionTenantConfig): PolicyExpression;
|
|
55
|
+
/**
|
|
56
|
+
* The rule a tenant declaration compiles to, or nothing when there is none.
|
|
57
|
+
*
|
|
58
|
+
* **Restrictive**, and that is the whole design. A restrictive policy is ANDed
|
|
59
|
+
* with every other policy on the table, so tenancy narrows what the
|
|
60
|
+
* collection's own `securityRules` allow and can never widen it. A permissive
|
|
61
|
+
* one would OR with them, and a single `access: "public"` rule elsewhere in the
|
|
62
|
+
* file would take the entire tenancy boundary off without contradicting
|
|
63
|
+
* anything a reader could see.
|
|
64
|
+
*
|
|
65
|
+
* One rule with `operation: "all"` rather than four with `operations: [...]`:
|
|
66
|
+
* `FOR ALL` gives Postgres the USING clause for SELECT/UPDATE/DELETE and the
|
|
67
|
+
* WITH CHECK clause for INSERT/UPDATE, which is exactly the coverage wanted,
|
|
68
|
+
* as one policy with one name instead of four.
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildTenantSecurityRule(collection: CollectionConfig): SecurityRule | undefined;
|
|
71
|
+
/** Why a write was refused by tenancy. */
|
|
72
|
+
export interface TenantWriteRefusal {
|
|
73
|
+
code: "TENANT_REQUIRED" | "TENANT_MISMATCH" | "TENANT_IMMUTABLE";
|
|
74
|
+
/** The property, for a `violations` entry and for the message. */
|
|
75
|
+
field: string;
|
|
76
|
+
message: string;
|
|
77
|
+
}
|
|
78
|
+
/** What {@link resolveTenantWrite} decided. */
|
|
79
|
+
export type TenantWriteDecision =
|
|
80
|
+
/** The values to write, with the tenant stamped if it was missing. */
|
|
81
|
+
{
|
|
82
|
+
values: Record<string, unknown>;
|
|
83
|
+
refusal?: undefined;
|
|
84
|
+
} | {
|
|
85
|
+
refusal: TenantWriteRefusal;
|
|
86
|
+
values?: undefined;
|
|
87
|
+
};
|
|
88
|
+
export interface TenantWriteInput {
|
|
89
|
+
tenant: CollectionTenantConfig;
|
|
90
|
+
/** The write's values, after defaults and hooks. */
|
|
91
|
+
values: Record<string, unknown>;
|
|
92
|
+
status: EntityStatus;
|
|
93
|
+
/**
|
|
94
|
+
* Every tenant the caller may write into.
|
|
95
|
+
*
|
|
96
|
+
* One entry for a claim, however many memberships they hold for the
|
|
97
|
+
* membership form, and none for a caller carrying neither.
|
|
98
|
+
*/
|
|
99
|
+
callerTenants: readonly unknown[];
|
|
100
|
+
/**
|
|
101
|
+
* Whether `callerTenants` is the whole list.
|
|
102
|
+
*
|
|
103
|
+
* A membership lookup is capped — a caller with more memberships than the
|
|
104
|
+
* cap would otherwise make every write of theirs a large read. When the cap
|
|
105
|
+
* is hit this is `false`, and a value that is not in the list is **let
|
|
106
|
+
* through** rather than refused: the list is no longer evidence of absence,
|
|
107
|
+
* and the policy's `WITH CHECK` is what actually decides. The API check is
|
|
108
|
+
* an earlier, clearer refusal of the same writes, never a second authority.
|
|
109
|
+
*/
|
|
110
|
+
callerTenantsComplete?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* True when tenancy does not apply to this caller — a bypass role, or the
|
|
113
|
+
* trusted server context. The same set the policy lets through, so the API
|
|
114
|
+
* and the database refuse the same writes.
|
|
115
|
+
*/
|
|
116
|
+
bypass: boolean;
|
|
117
|
+
/** The row's current values, on an update. */
|
|
118
|
+
previousValues?: Record<string, unknown>;
|
|
119
|
+
/** The collection slug, for the message. */
|
|
120
|
+
slug: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Stamp, or refuse, the tenant on a write.
|
|
124
|
+
*
|
|
125
|
+
* Three refusals, and each exists because the alternative lands somewhere
|
|
126
|
+
* worse:
|
|
127
|
+
*
|
|
128
|
+
* - **`TENANT_REQUIRED`** — the caller has no tenant, or belongs to several and
|
|
129
|
+
* named none. Stamping a guess would put the row in the wrong tenant; letting
|
|
130
|
+
* it through would write a NULL into a `NOT NULL` column and surface as a
|
|
131
|
+
* 23502 naming a column the caller never wrote.
|
|
132
|
+
* - **`TENANT_MISMATCH`** — the caller named a tenant that is not theirs. The
|
|
133
|
+
* database refuses this too, through the policy's `WITH CHECK`, but as a
|
|
134
|
+
* 42501 "new row violates row-level security policy" with no mention of which
|
|
135
|
+
* field or why. Refused here so the answer names the field.
|
|
136
|
+
* - **`TENANT_IMMUTABLE`** — an update that moves a row to another tenant. RLS
|
|
137
|
+
* would allow it whenever the caller belongs to both, and it is almost never
|
|
138
|
+
* what anybody meant: it takes the row out of one tenant's history and drops
|
|
139
|
+
* it into another's, with no trace on either side. A deliberate move is a
|
|
140
|
+
* `bypassRoles` operation.
|
|
141
|
+
*
|
|
142
|
+
* A bypass caller is exempt from all three: they are trusted across tenants by
|
|
143
|
+
* declaration, and stamping their write would silently confine a support
|
|
144
|
+
* operator's row to whichever tenant they happen to carry.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveTenantWrite(input: TenantWriteInput): TenantWriteDecision;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2-canary.g09316f6",
|
|
4
4
|
"description": "Rebase shared core — collection registry, data driver adapter and fluent query builder. No React dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"fast-equals": "6.0.2",
|
|
46
46
|
"json-logic-js": "^2.0.5",
|
|
47
|
-
"@rebasepro/types": "0.19.
|
|
48
|
-
"@rebasepro/utils": "0.19.
|
|
47
|
+
"@rebasepro/types": "0.19.2-canary.g09316f6",
|
|
48
|
+
"@rebasepro/utils": "0.19.2-canary.g09316f6"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@jest/globals": "^30.4.1",
|