@rebasepro/common 0.6.1 → 0.8.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/collections/CollectionRegistry.d.ts +30 -2
- package/dist/collections/default-collections.d.ts +255 -2
- package/dist/data/buildRoutedRebaseData.d.ts +53 -0
- package/dist/data/filter-dialect.d.ts +61 -0
- package/dist/data/query_builder.d.ts +4 -4
- package/dist/data/resolveDataSource.d.ts +43 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +777 -178
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +793 -176
- package/dist/index.umd.js.map +1 -1
- package/dist/table-classification.d.ts +47 -0
- package/dist/util/builders.d.ts +48 -1
- package/dist/util/callbacks.d.ts +6 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/permissions.d.ts +26 -2
- package/dist/util/policy/evaluatePolicy.d.ts +31 -0
- package/dist/util/policy/index.d.ts +3 -0
- package/dist/util/policy/policyToPostgres.d.ts +10 -0
- package/dist/util/policy/securityRuleToConditions.d.ts +24 -0
- package/dist/util/policy/sqlToPolicy.d.ts +20 -0
- package/dist/util/storage.d.ts +26 -1
- package/package.json +3 -3
- package/src/collections/CollectionRegistry.ts +80 -16
- package/src/collections/default-collections.ts +4 -4
- package/src/data/buildRebaseData.ts +9 -120
- package/src/data/buildRoutedRebaseData.ts +97 -0
- package/src/data/filter-dialect.ts +318 -0
- package/src/data/query_builder.ts +10 -10
- package/src/data/resolveDataSource.ts +79 -0
- package/src/index.ts +4 -1
- package/src/table-classification.ts +109 -0
- package/src/util/builders.ts +78 -1
- package/src/util/callbacks.ts +8 -1
- package/src/util/index.ts +1 -0
- package/src/util/permissions.test.ts +5 -3
- package/src/util/permissions.ts +85 -158
- package/src/util/policy/evaluatePolicy.ts +146 -0
- package/src/util/policy/index.ts +3 -0
- package/src/util/policy/policyToPostgres.ts +85 -0
- package/src/util/policy/securityRuleToConditions.ts +67 -0
- package/src/util/policy/sqlToPolicy.ts +88 -0
- package/src/util/references.ts +1 -1
- package/src/util/relations.ts +8 -9
- package/src/util/resolutions.ts +6 -6
- package/src/util/storage.ts +34 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Classification
|
|
3
|
+
*
|
|
4
|
+
* Shared constants and pure functions for classifying database tables.
|
|
5
|
+
* Used by both the server-side PostgresBackendDriver and the Studio RLS editor.
|
|
6
|
+
*/
|
|
7
|
+
/** Possible categories a database table can belong to. */
|
|
8
|
+
export type TableCategory = "rebase-internal" | "junction" | "user";
|
|
9
|
+
/** Schemas that are always considered Rebase-internal. */
|
|
10
|
+
export declare const REBASE_INTERNAL_SCHEMAS: readonly string[];
|
|
11
|
+
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
12
|
+
export declare const REBASE_INTERNAL_PREFIXES: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Synchronously classify a table based on naming conventions.
|
|
15
|
+
*
|
|
16
|
+
* @param tableName - The unqualified name of the table.
|
|
17
|
+
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
18
|
+
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
19
|
+
* carries a reserved prefix; `"user"` otherwise.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* Junction-table detection requires an async database query and is therefore
|
|
23
|
+
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
24
|
+
* the set of junction tables, then reclassify as needed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyTable(tableName: string, schemaName: string): TableCategory;
|
|
27
|
+
/**
|
|
28
|
+
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
29
|
+
*
|
|
30
|
+
* @param tableName - The unqualified name of the table.
|
|
31
|
+
* @param schemaName - The schema the table belongs to.
|
|
32
|
+
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isRebaseInternalTable(tableName: string, schemaName: string): boolean;
|
|
35
|
+
/** SQL query that detects junction tables in the `public` schema. */
|
|
36
|
+
export declare const JUNCTION_TABLES_SQL = "\n SELECT t.table_name\n FROM information_schema.tables t\n WHERE t.table_schema = 'public'\n AND t.table_type = 'BASE TABLE'\n AND NOT EXISTS (\n SELECT 1\n FROM information_schema.columns c\n WHERE c.table_schema = t.table_schema\n AND c.table_name = t.table_name\n AND c.column_name NOT IN (\n SELECT kcu.column_name\n FROM information_schema.key_column_usage kcu\n JOIN information_schema.table_constraints tc\n ON tc.constraint_name = kcu.constraint_name\n AND tc.table_schema = kcu.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY'\n AND kcu.table_schema = t.table_schema\n AND kcu.table_name = t.table_name\n )\n )\n";
|
|
37
|
+
/**
|
|
38
|
+
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
39
|
+
*
|
|
40
|
+
* A junction table is defined as a table where **every** column participates in
|
|
41
|
+
* at least one foreign-key constraint.
|
|
42
|
+
*
|
|
43
|
+
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
44
|
+
* resulting rows.
|
|
45
|
+
* @returns A `Set` containing the names of all detected junction tables.
|
|
46
|
+
*/
|
|
47
|
+
export declare function detectJunctionTables(executeSql: (sql: string) => Promise<Record<string, unknown>[]>): Promise<Set<string>>;
|
package/dist/util/builders.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdditionalFieldDelegate, ArrayProperty, BooleanProperty, DateProperty, EntityCallbacks, EntityCollection, EnumValueConfig, EnumValues, GeopointProperty, MapProperty, NumberProperty, Properties, Property, ReferenceProperty, StringProperty, User } from "@rebasepro/types";
|
|
1
|
+
import { AdditionalFieldDelegate, ArrayProperty, BooleanProperty, DateProperty, EntityCallbacks, EntityCollection, EnumValueConfig, EnumValues, FirebaseCollection, FirebaseProperties, GeopointProperty, InferEntityType, MapProperty, MongoDBCollection, MongoProperties, NumberProperty, PostgresCollection, PostgresProperties, Properties, Property, ReferenceProperty, StringProperty, User } from "@rebasepro/types";
|
|
2
2
|
/**
|
|
3
3
|
* Identity function we use to defeat the type system of Typescript and build
|
|
4
4
|
* collection views with all its properties
|
|
@@ -6,6 +6,53 @@ import { AdditionalFieldDelegate, ArrayProperty, BooleanProperty, DateProperty,
|
|
|
6
6
|
* @group Builder
|
|
7
7
|
*/
|
|
8
8
|
export declare function buildCollection<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(collection: EntityCollection<M, USER>): EntityCollection<M, USER>;
|
|
9
|
+
/**
|
|
10
|
+
* Define a PostgreSQL-backed collection with full type inference.
|
|
11
|
+
*
|
|
12
|
+
* The `const P` generic captures literal property types from your
|
|
13
|
+
* `properties` object, which enables autocomplete on `titleProperty`,
|
|
14
|
+
* `sort`, `propertiesOrder`, `fixedFilter`, and entity callbacks.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const products = defineCollection({
|
|
19
|
+
* name: "Products",
|
|
20
|
+
* slug: "products",
|
|
21
|
+
* table: "products",
|
|
22
|
+
* properties: {
|
|
23
|
+
* name: { name: "Name", type: "string", validation: { required: true } },
|
|
24
|
+
* price: { name: "Price", type: "number" },
|
|
25
|
+
* },
|
|
26
|
+
* titleProperty: "name", // ✅ autocomplete: "name" | "price"
|
|
27
|
+
* sort: ["price", "asc"], // ✅ autocomplete on first element
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @group Builder
|
|
32
|
+
*/
|
|
33
|
+
export declare function defineCollection<const P extends PostgresProperties, USER extends User = User>(collection: Omit<PostgresCollection<InferEntityType<P>, USER>, "properties"> & {
|
|
34
|
+
properties: P;
|
|
35
|
+
}): PostgresCollection<InferEntityType<P>, USER> & {
|
|
36
|
+
properties: P;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Define a Firestore-backed collection with full type inference.
|
|
40
|
+
* @group Builder
|
|
41
|
+
*/
|
|
42
|
+
export declare function defineCollection<const P extends FirebaseProperties, USER extends User = User>(collection: Omit<FirebaseCollection<InferEntityType<P>, USER>, "properties"> & {
|
|
43
|
+
properties: P;
|
|
44
|
+
}): FirebaseCollection<InferEntityType<P>, USER> & {
|
|
45
|
+
properties: P;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Define a MongoDB-backed collection with full type inference.
|
|
49
|
+
* @group Builder
|
|
50
|
+
*/
|
|
51
|
+
export declare function defineCollection<const P extends MongoProperties, USER extends User = User>(collection: Omit<MongoDBCollection<InferEntityType<P>, USER>, "properties"> & {
|
|
52
|
+
properties: P;
|
|
53
|
+
}): MongoDBCollection<InferEntityType<P>, USER> & {
|
|
54
|
+
properties: P;
|
|
55
|
+
};
|
|
9
56
|
/**
|
|
10
57
|
* Identity function we use to defeat the type system of Typescript and preserve
|
|
11
58
|
* the property keys.
|
package/dist/util/callbacks.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { EntityCallbacks, Properties } from "@rebasepro/types";
|
|
1
|
+
import { EntityCallbacks, Properties, RebaseCallContext } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Context passed to entity lifecycle callbacks.
|
|
4
|
+
* @group Models
|
|
5
|
+
*/
|
|
6
|
+
export type EntityCallbackContext = RebaseCallContext;
|
|
2
7
|
/**
|
|
3
8
|
* Helper function to extract field-level PropertyCallbacks from a properties schema
|
|
4
9
|
* and wrap them into an EntityCallbacks object recursively.
|
package/dist/util/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Entity, EntityCollection, User } from "@rebasepro/types";
|
|
1
|
+
import { Entity, EntityCollection, SecurityOperation, User } from "@rebasepro/types";
|
|
2
2
|
/**
|
|
3
3
|
* Minimal auth context for permission checking.
|
|
4
4
|
* Only requires the user object — avoids forcing callers to construct
|
|
@@ -7,7 +7,31 @@ import { Entity, EntityCollection, User } from "@rebasepro/types";
|
|
|
7
7
|
export interface AuthContext<USER extends User = User> {
|
|
8
8
|
user: USER | null;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* How to resolve a policy result that cannot be decided client-side (a raw-SQL
|
|
12
|
+
* escape-hatch rule, or a row-column reference with no row in hand).
|
|
13
|
+
*
|
|
14
|
+
* - `"allow"` (default): optimistic — used for admin-UI gating, where Postgres
|
|
15
|
+
* remains the authoritative gate and hiding a working action is worse than
|
|
16
|
+
* showing one the server may reject.
|
|
17
|
+
* - `"deny"`: fail-closed — used by real enforcement callers (e.g. a driver
|
|
18
|
+
* applying policies in-process), so an undecidable rule never silently allows.
|
|
19
|
+
*/
|
|
20
|
+
export type UnknownResolution = "allow" | "deny";
|
|
21
|
+
export interface CheckOperationOptions {
|
|
22
|
+
onUnknown?: UnknownResolution;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Decide whether an operation is permitted for a user on a (possibly null) row,
|
|
26
|
+
* by evaluating the collection's security rules with the shared policy model —
|
|
27
|
+
* the same model compiled to Postgres RLS DDL, so the decision matches database
|
|
28
|
+
* enforcement for every non-raw rule.
|
|
29
|
+
*
|
|
30
|
+
* @param options.onUnknown how to treat rules that cannot be decided
|
|
31
|
+
* client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
|
|
32
|
+
* for optimistic UI gating; enforcement callers should pass `"deny"`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkOperation<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, entity: Entity<M> | null, targetOperation: SecurityOperation, options?: CheckOperationOptions): boolean;
|
|
11
35
|
export declare function canReadCollection<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>): boolean;
|
|
12
36
|
export declare function canEditEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
|
|
13
37
|
export declare function canCreateEntity<M extends Record<string, unknown>, USER extends User>(collection: EntityCollection<M>, authContext: AuthContext<USER>, path: string, entity: Entity<M> | null): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Entity, PolicyExpression } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Result of evaluating a policy client-side. `"unknown"` means the expression
|
|
4
|
+
* could not be decided without more information — either a raw-SQL escape-hatch
|
|
5
|
+
* node (which the client deliberately never guesses) or a row-column reference
|
|
6
|
+
* with no entity in hand (e.g. list-level gating). Callers decide how to resolve
|
|
7
|
+
* `"unknown"`: fail-closed for an enforcement decision, optimistic for pure
|
|
8
|
+
* visibility gating.
|
|
9
|
+
*/
|
|
10
|
+
export type TriState = boolean | "unknown";
|
|
11
|
+
/**
|
|
12
|
+
* Context for {@link evaluatePolicy}: the acting user (or none) and the row
|
|
13
|
+
* being evaluated (or none, for collection-level gating).
|
|
14
|
+
*/
|
|
15
|
+
export interface PolicyEvalContext {
|
|
16
|
+
/** The current user's id, or null/undefined when unauthenticated. */
|
|
17
|
+
uid?: string | null;
|
|
18
|
+
/** The current user's application roles. */
|
|
19
|
+
roles?: string[];
|
|
20
|
+
/** The row being evaluated, or null when no specific row is available. */
|
|
21
|
+
entity: Entity | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Evaluates a {@link PolicyExpression} against a user + row, using three-valued
|
|
25
|
+
* (Kleene) logic so that `"unknown"` sub-results propagate soundly.
|
|
26
|
+
*
|
|
27
|
+
* This is the JavaScript twin of {@link policyToPostgres}: both derive from the
|
|
28
|
+
* same expression, so the admin UI matches database enforcement by construction
|
|
29
|
+
* for every non-raw rule.
|
|
30
|
+
*/
|
|
31
|
+
export declare function evaluatePolicy(expr: PolicyExpression, ctx: PolicyEvalContext): TriState;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EntityCollection, PolicyExpression } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Compiles a {@link PolicyExpression} to a PostgreSQL boolean SQL string,
|
|
4
|
+
* suitable for a `USING (...)` / `WITH CHECK (...)` clause.
|
|
5
|
+
*
|
|
6
|
+
* This is one of the two consumers of the shared policy model (the other being
|
|
7
|
+
* {@link evaluatePolicy}); the Postgres schema generators call it so that DDL
|
|
8
|
+
* and the admin UI derive from the exact same expression.
|
|
9
|
+
*/
|
|
10
|
+
export declare function policyToPostgres(expr: PolicyExpression, collection?: EntityCollection): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PolicyExpression, SecurityRule } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* The normalized `USING` / `WITH CHECK` conditions for a single security rule,
|
|
4
|
+
* expressed in the engine-agnostic {@link PolicyExpression} model.
|
|
5
|
+
*
|
|
6
|
+
* A `null` clause means "this rule contributes no condition for that clause";
|
|
7
|
+
* consumers apply the default (Postgres denies with `false`).
|
|
8
|
+
*/
|
|
9
|
+
export interface RuleConditions {
|
|
10
|
+
usingExpr: PolicyExpression | null;
|
|
11
|
+
withCheckExpr: PolicyExpression | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Desugars a {@link SecurityRule} — its `access`/`ownerField`/`roles` shortcuts,
|
|
15
|
+
* structured `condition`/`check`, and raw `using`/`withCheck` — into a single
|
|
16
|
+
* normalized {@link PolicyExpression} pair.
|
|
17
|
+
*
|
|
18
|
+
* **This is the linchpin against drift:** both the Postgres DDL generators and
|
|
19
|
+
* the client-side evaluator consume this one function, so there is exactly one
|
|
20
|
+
* definition of what a rule means. In particular, application `roles` are folded
|
|
21
|
+
* into the expression here (AND'd with the base condition, matching how Postgres
|
|
22
|
+
* generates the clause) rather than being handled separately by each consumer.
|
|
23
|
+
*/
|
|
24
|
+
export declare function securityRuleToConditions(rule: SecurityRule): RuleConditions;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PolicyExpression } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* A tiny, regex-based SQL "parser" for security rules.
|
|
4
|
+
*
|
|
5
|
+
* This is NOT a full SQL parser. It is designed to handle the subset of SQL
|
|
6
|
+
* commonly used in `USING` and `WITH CHECK` clauses, enough to drive the
|
|
7
|
+
* optimistic client-side UI decision.
|
|
8
|
+
*
|
|
9
|
+
* It handles:
|
|
10
|
+
* - `field = 'literal'`
|
|
11
|
+
* - `field != 'literal'`
|
|
12
|
+
* - `field = current_setting('app.user_id')`
|
|
13
|
+
* - `A AND B`
|
|
14
|
+
* - `true`
|
|
15
|
+
* - `IN (...)` (as optimistic true)
|
|
16
|
+
*
|
|
17
|
+
* For anything it doesn't understand, it returns a `raw` expression, which
|
|
18
|
+
* the evaluator treats as "unknown" (and usually optimistic true).
|
|
19
|
+
*/
|
|
20
|
+
export declare function sqlToPolicy(sql: string): PolicyExpression;
|
package/dist/util/storage.d.ts
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
import { ArrayProperty, EntityValues, StorageConfig, StringProperty, UploadedFileContext } from "@rebasepro/types";
|
|
1
|
+
import { ArrayProperty, EntityValues, StorageConfig, StorageSource, StorageSourceRegistry, StringProperty, UploadedFileContext } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the {@link StorageSource} to use for a property, given the key
|
|
4
|
+
* referenced by `StorageConfig.storageSource`.
|
|
5
|
+
*
|
|
6
|
+
* Resolution priority:
|
|
7
|
+
* 1. No `sourceKey` → the default source (backward compatible).
|
|
8
|
+
* 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
|
|
9
|
+
* 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
|
|
10
|
+
* 4. Fall back to the default source.
|
|
11
|
+
*
|
|
12
|
+
* Shared by the upload hook, the markdown editor, and the read-only previews
|
|
13
|
+
* so the resolution logic lives in one place.
|
|
14
|
+
*
|
|
15
|
+
* @group Storage
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveStorageSource(params: {
|
|
18
|
+
/** Key from `StorageConfig.storageSource`. */
|
|
19
|
+
sourceKey?: string | null;
|
|
20
|
+
/** Built sources keyed by storage-source key (e.g. from context). */
|
|
21
|
+
sources?: Record<string, StorageSource>;
|
|
22
|
+
/** Optional explicit registry — takes precedence over `sources`. */
|
|
23
|
+
registry?: StorageSourceRegistry;
|
|
24
|
+
/** Default source, used when no key is set or the key cannot be resolved. */
|
|
25
|
+
defaultSource: StorageSource;
|
|
26
|
+
}): StorageSource;
|
|
2
27
|
interface ResolveFilenameStringParams<M extends Record<string, unknown>> {
|
|
3
28
|
input: string | ((context: UploadedFileContext) => (Promise<string> | string));
|
|
4
29
|
storage: StorageConfig;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"fast-equals": "6.0.0",
|
|
43
43
|
"json-logic-js": "^2.0.5",
|
|
44
|
-
"@rebasepro/
|
|
45
|
-
"@rebasepro/
|
|
44
|
+
"@rebasepro/utils": "0.8.0",
|
|
45
|
+
"@rebasepro/types": "0.8.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@jest/globals": "^30.4.1",
|
|
@@ -1,23 +1,61 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrayProperty,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
EntityCallbacks,
|
|
4
|
+
EngineProperties,
|
|
5
5
|
EntityCollection,
|
|
6
|
+
getDataSourceCapabilities,
|
|
7
|
+
getDeclaredSubcollections,
|
|
6
8
|
NumberProperty,
|
|
7
9
|
Properties,
|
|
8
10
|
Property,
|
|
9
11
|
Relation,
|
|
10
12
|
RelationProperty,
|
|
11
|
-
StringProperty
|
|
12
|
-
getDataSourceCapabilities
|
|
13
|
+
StringProperty
|
|
13
14
|
} from "@rebasepro/types";
|
|
14
15
|
import { deepEqual } from "fast-equals";
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
-
|
|
17
|
+
import {
|
|
18
|
+
enumToObjectEntries,
|
|
19
|
+
findRelation,
|
|
20
|
+
getSubcollections,
|
|
21
|
+
getTableName,
|
|
22
|
+
resolveCollectionRelations,
|
|
23
|
+
sanitizeRelation
|
|
24
|
+
} from "../util";
|
|
25
|
+
import { deepClone, mergeDeep, removeFunctions } from "@rebasepro/utils";
|
|
26
|
+
import { DataSourceRegistry, resolveDataSource } from "../data/resolveDataSource";
|
|
18
27
|
|
|
19
28
|
export class CollectionRegistry {
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Declared data sources, used during normalization to resolve each
|
|
32
|
+
* collection's engine (so `dataSource`-only collections get the right
|
|
33
|
+
* capabilities). Empty by default.
|
|
34
|
+
*/
|
|
35
|
+
private dataSources: DataSourceRegistry = {};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Global lifecycle callbacks applied to every collection.
|
|
39
|
+
* Runs on all data paths (REST, WebSocket, `rebase.data`).
|
|
40
|
+
* Execution order: global → collection → property callbacks.
|
|
41
|
+
*/
|
|
42
|
+
private _globalCallbacks?: EntityCallbacks;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set global lifecycle callbacks that apply to every collection.
|
|
46
|
+
* Typically called once during backend initialization.
|
|
47
|
+
*/
|
|
48
|
+
setGlobalCallbacks(callbacks: EntityCallbacks): void {
|
|
49
|
+
this._globalCallbacks = callbacks;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the currently registered global callbacks, if any.
|
|
54
|
+
*/
|
|
55
|
+
getGlobalCallbacks(): EntityCallbacks | undefined {
|
|
56
|
+
return this._globalCallbacks;
|
|
57
|
+
}
|
|
58
|
+
|
|
21
59
|
// Normalized runtime layer (used by Data Grid / UI)
|
|
22
60
|
private collectionsByTableName = new Map<string, EntityCollection>();
|
|
23
61
|
private collectionsBySlug = new Map<string, EntityCollection>();
|
|
@@ -34,12 +72,24 @@ export class CollectionRegistry {
|
|
|
34
72
|
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
35
73
|
private lastRawInputSnapshot: ReturnType<typeof removeFunctions>[] | null = null;
|
|
36
74
|
|
|
37
|
-
constructor(collections?: EntityCollection[]) {
|
|
75
|
+
constructor(collections?: EntityCollection[], dataSources?: DataSourceRegistry) {
|
|
76
|
+
if (dataSources) this.dataSources = dataSources;
|
|
38
77
|
if (collections) {
|
|
39
78
|
this.registerMultiple(collections);
|
|
40
79
|
}
|
|
41
80
|
}
|
|
42
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Provide the declared data sources used to resolve each collection's
|
|
84
|
+
* engine during normalization. Set this before registering collections.
|
|
85
|
+
* Returns true if the registry changed (callers may re-register).
|
|
86
|
+
*/
|
|
87
|
+
setDataSources(dataSources: DataSourceRegistry): boolean {
|
|
88
|
+
if (deepEqual(this.dataSources, dataSources)) return false;
|
|
89
|
+
this.dataSources = dataSources ?? {};
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
reset() {
|
|
44
94
|
this.collectionsByTableName.clear();
|
|
45
95
|
this.collectionsBySlug.clear();
|
|
@@ -163,12 +213,24 @@ export class CollectionRegistry {
|
|
|
163
213
|
// and for preventing mutation of module-level collection singletons.
|
|
164
214
|
const result = { ...collection } as EntityCollection;
|
|
165
215
|
|
|
216
|
+
// 0. Resolve and stamp `dataSource` and `engine` on the normalized copy.
|
|
217
|
+
// After this block every normalized collection has both fields set,
|
|
218
|
+
// so downstream code can read them directly without calling
|
|
219
|
+
// `resolveDataSource()`. Only the normalized layer is affected —
|
|
220
|
+
// the raw layer used by the collection editor keeps the author's
|
|
221
|
+
// original fields.
|
|
222
|
+
{
|
|
223
|
+
const resolved = resolveDataSource(result, this.dataSources);
|
|
224
|
+
if (!result.dataSource) (result as { dataSource?: string }).dataSource = resolved.key;
|
|
225
|
+
if (!result.engine) (result as { engine?: string }).engine = resolved.engine;
|
|
226
|
+
}
|
|
227
|
+
|
|
166
228
|
// 1. Extract relations from properties that have inline config (target set)
|
|
167
229
|
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
168
230
|
|
|
169
231
|
// 2. Merge with manual relations[] (manual entries win on name conflict)
|
|
170
|
-
const relResult = result
|
|
171
|
-
const manualRelations = getDataSourceCapabilities(result.
|
|
232
|
+
const relResult = result;
|
|
233
|
+
const manualRelations = getDataSourceCapabilities(result.engine).supportsRelations ? (relResult.relations ?? []) : [];
|
|
172
234
|
const mergedRelationsRaw = [...extractedRelations];
|
|
173
235
|
for (const manual of manualRelations) {
|
|
174
236
|
const name = manual.relationName;
|
|
@@ -194,7 +256,7 @@ export class CollectionRegistry {
|
|
|
194
256
|
// foreignKeyOnTarget, etc.) are populated. Without this the
|
|
195
257
|
// property.relation stamp is missing junction-table metadata and
|
|
196
258
|
// the backend cannot fetch many-to-many data.
|
|
197
|
-
if (getDataSourceCapabilities(result.
|
|
259
|
+
if (getDataSourceCapabilities(result.engine).supportsRelations) {
|
|
198
260
|
mergedRelations = mergedRelationsRaw.map(r => {
|
|
199
261
|
try {
|
|
200
262
|
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
@@ -211,13 +273,15 @@ export class CollectionRegistry {
|
|
|
211
273
|
|
|
212
274
|
// 4. Normalize properties (which stamps relation on each property)
|
|
213
275
|
const properties: Properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
214
|
-
result.properties = properties;
|
|
276
|
+
result.properties = properties as EngineProperties;
|
|
215
277
|
|
|
216
278
|
// Populate childCollections from driver-specific fields
|
|
217
279
|
if (!result.childCollections) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
280
|
+
const capabilities = getDataSourceCapabilities(result.engine);
|
|
281
|
+
const declaredSubcollections = getDeclaredSubcollections(result);
|
|
282
|
+
if (capabilities.supportsSubcollections && declaredSubcollections) {
|
|
283
|
+
result.childCollections = declaredSubcollections;
|
|
284
|
+
} else if (capabilities.supportsRelations && relResult.relations) {
|
|
221
285
|
const manyRelations = relResult.relations.filter((r: Relation) => r.cardinality === "many");
|
|
222
286
|
if (manyRelations.length > 0) {
|
|
223
287
|
result.childCollections = () => manyRelations.map((r: Relation) => {
|
|
@@ -378,8 +442,8 @@ export class CollectionRegistry {
|
|
|
378
442
|
const relationKey = pathSegments[i];
|
|
379
443
|
|
|
380
444
|
// Get relations for current collection
|
|
381
|
-
if (!getDataSourceCapabilities(currentCollection.
|
|
382
|
-
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses
|
|
445
|
+
if (!getDataSourceCapabilities(currentCollection.engine).supportsRelations) {
|
|
446
|
+
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses engine '${currentCollection.engine}'`);
|
|
383
447
|
}
|
|
384
448
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
385
449
|
const relation = findRelation(resolvedRelations, relationKey);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { defineCollection } from "../util/builders";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default users collection.
|
|
@@ -7,7 +7,7 @@ import type { PostgresCollection } from "@rebasepro/types";
|
|
|
7
7
|
* Slug-based dedup (Map keyed by slug, last-write-wins) lets developers
|
|
8
8
|
* override by defining their own collection with `slug: "users"`.
|
|
9
9
|
*/
|
|
10
|
-
export const defaultUsersCollection
|
|
10
|
+
export const defaultUsersCollection = defineCollection({
|
|
11
11
|
name: "Users",
|
|
12
12
|
singularName: "User",
|
|
13
13
|
slug: "users",
|
|
@@ -48,7 +48,7 @@ unique: true }
|
|
|
48
48
|
name: "Photo URL",
|
|
49
49
|
type: "string",
|
|
50
50
|
columnName: "photo_url",
|
|
51
|
-
url: "image"
|
|
51
|
+
ui: { url: "image" }
|
|
52
52
|
},
|
|
53
53
|
roles: {
|
|
54
54
|
name: "Roles",
|
|
@@ -120,4 +120,4 @@ disabled: { hidden: true } }
|
|
|
120
120
|
},
|
|
121
121
|
listProperties: ["displayName", "email", "roles", "createdAt"],
|
|
122
122
|
propertiesOrder: ["id", "email", "displayName", "roles", "createdAt"]
|
|
123
|
-
};
|
|
123
|
+
});
|