@rebasepro/server-postgres 0.10.1-canary.b1e3dbf → 0.10.1-canary.ed8caed
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/chunk-DSJWtz9O.js +40 -0
- package/dist/ensure-collection-tables-BVvtkkRm.js +304 -0
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
- package/dist/index.es.js +499 -4644
- package/dist/index.es.js.map +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +79 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -1
- package/dist/services/FetchService.d.ts +21 -8
- package/dist/services/PersistService.d.ts +12 -0
- package/dist/services/RelationService.d.ts +30 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/src-CBgtrPhJ.js +336 -0
- package/dist/src-CBgtrPhJ.js.map +1 -0
- package/dist/src-lcfUP4xg.js +4106 -0
- package/dist/src-lcfUP4xg.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +41 -0
- package/package.json +8 -9
- package/src/PostgresBootstrapper.ts +40 -0
- package/src/schema/doctor.ts +6 -6
- package/src/schema/ensure-collection-tables.test.ts +156 -0
- package/src/schema/ensure-collection-tables.ts +297 -0
- package/src/schema/generate-drizzle-schema-logic.ts +13 -9
- package/src/schema/generate-postgres-ddl-logic.ts +22 -15
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +6 -6
- package/src/services/FetchService.ts +104 -114
- package/src/services/PersistService.ts +127 -85
- package/src/services/RelationService.ts +95 -6
- package/src/services/nested-path.ts +130 -0
- package/src/utils/drizzle-conditions.ts +143 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bringing a database up to date with a bundle's collections, additively.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* A managed runtime boots someone else's compiled project against a database it
|
|
7
|
+
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
8
|
+
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
9
|
+
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
10
|
+
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
11
|
+
*
|
|
12
|
+
* ## Why additive-only, forever
|
|
13
|
+
*
|
|
14
|
+
* This runs unattended, against a database with customers' data in it, with no
|
|
15
|
+
* human reading a diff. So it may only ever do things that cannot lose data:
|
|
16
|
+
* create a missing table, add a missing column, create a missing enum type.
|
|
17
|
+
*
|
|
18
|
+
* It will **never** drop a table or a column, narrow a type, or alter a
|
|
19
|
+
* constraint. A removed field leaves its column behind; a renamed field looks
|
|
20
|
+
* like an addition and the old column stays. That is the correct trade for an
|
|
21
|
+
* automated path — the alternative is an unattended process that can silently
|
|
22
|
+
* destroy a column, which is precisely the failure `db push` was hardened
|
|
23
|
+
* against. Destructive changes stay a deliberate, human-reviewed migration.
|
|
24
|
+
*
|
|
25
|
+
* Because of that, this is safe to run on every boot, and re-running it is a
|
|
26
|
+
* no-op.
|
|
27
|
+
*/
|
|
28
|
+
import { type CollectionConfig } from "@rebasepro/types";
|
|
29
|
+
/**
|
|
30
|
+
* The subset of a database handle this needs: run a statement, get rows back.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately parameterless. Everything here is DDL or catalogue reads keyed by
|
|
33
|
+
* schema name, and schema names are identifiers — they cannot be bound as
|
|
34
|
+
* parameters anyway. They are validated against {@link SAFE_IDENTIFIER} before
|
|
35
|
+
* they reach a statement, so a config that somehow carried a quote is refused
|
|
36
|
+
* rather than concatenated.
|
|
37
|
+
*/
|
|
38
|
+
export interface Queryable {
|
|
39
|
+
query<T = unknown>(sql: string): Promise<{
|
|
40
|
+
rows: T[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** What the database currently has, as the planner needs it. */
|
|
44
|
+
export interface ExistingSchema {
|
|
45
|
+
/** `schema.table` → set of column names. */
|
|
46
|
+
tables: Map<string, Set<string>>;
|
|
47
|
+
/** `schema.typename` of every enum type that already exists. */
|
|
48
|
+
enums: Set<string>;
|
|
49
|
+
}
|
|
50
|
+
export interface EnsureAction {
|
|
51
|
+
kind: "create-enum" | "create-table" | "add-column";
|
|
52
|
+
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
53
|
+
target: string;
|
|
54
|
+
sql: string;
|
|
55
|
+
}
|
|
56
|
+
export interface EnsurePlan {
|
|
57
|
+
actions: EnsureAction[];
|
|
58
|
+
/** Every statement, in dependency order. Empty when the schema is current. */
|
|
59
|
+
statements: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
63
|
+
*
|
|
64
|
+
* Ordering matters and is deliberate: enum types before the tables and columns
|
|
65
|
+
* that reference them, tables before the columns added to other tables (a new
|
|
66
|
+
* table may be the target of a relation), and nothing is emitted twice.
|
|
67
|
+
*/
|
|
68
|
+
export declare function planCollectionSchemaEnsure(collections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
|
|
69
|
+
/** Read what the database has, for the schemas the collections live in. */
|
|
70
|
+
export declare function readExistingSchema(client: Queryable, schemas: string[]): Promise<ExistingSchema>;
|
|
71
|
+
/**
|
|
72
|
+
* Bring the database up to date. Returns what it did.
|
|
73
|
+
*
|
|
74
|
+
* Each statement runs on its own rather than in one transaction: they are all
|
|
75
|
+
* independently safe and idempotent, and a single failure (an enum label that
|
|
76
|
+
* cannot be added, say) should not roll back the tables that were created fine.
|
|
77
|
+
* The error is surfaced with the statement that caused it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsurePlan>;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { CollectionConfig } from "@rebasepro/types";
|
|
1
|
+
import { CollectionConfig, Property } from "@rebasepro/types";
|
|
2
|
+
export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
|
|
3
|
+
export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
|
|
4
|
+
export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
|
|
2
5
|
export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
|
|
3
6
|
includePolicies?: boolean;
|
|
4
7
|
}) => Promise<string>;
|
|
@@ -5,6 +5,7 @@ import type { VectorSearchParams } from "@rebasepro/types";
|
|
|
5
5
|
import { RelationService } from "./RelationService";
|
|
6
6
|
import { DrizzleClient } from "../interfaces";
|
|
7
7
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
8
|
+
import { type NestedPathHop } from "./nested-path";
|
|
8
9
|
/**
|
|
9
10
|
* Service for handling all row read operations.
|
|
10
11
|
* Handles fetching, searching, counting, and filtering rows.
|
|
@@ -65,6 +66,22 @@ export declare class FetchService {
|
|
|
65
66
|
* Extract cursor pagination conditions from startAfter options.
|
|
66
67
|
*/
|
|
67
68
|
private buildCursorConditions;
|
|
69
|
+
/**
|
|
70
|
+
* Compile "rows reachable from this parent" into a `WHERE` condition on the
|
|
71
|
+
* target table, so a nested listing can run as an ordinary collection query.
|
|
72
|
+
*/
|
|
73
|
+
private buildRelationScope;
|
|
74
|
+
/**
|
|
75
|
+
* Whether `id` is actually reachable at `collectionPath`.
|
|
76
|
+
*
|
|
77
|
+
* Trivially true for a root path. For a nested one it is a real question:
|
|
78
|
+
* the path resolves to the target collection, and matching on the primary
|
|
79
|
+
* key alone made the parent segment decorative — `authors/1/posts/43`
|
|
80
|
+
* returned post 43 whoever wrote it, and the REST layer's delete then
|
|
81
|
+
* deleted it. A row that is not under this parent is reported as absent,
|
|
82
|
+
* which is what a caller addressing it through the parent should see.
|
|
83
|
+
*/
|
|
84
|
+
private isAddressableUnder;
|
|
68
85
|
/**
|
|
69
86
|
* Fetch a single row by ID
|
|
70
87
|
*/
|
|
@@ -83,6 +100,8 @@ export declare class FetchService {
|
|
|
83
100
|
databaseId?: string;
|
|
84
101
|
vectorSearch?: VectorSearchParams;
|
|
85
102
|
logical?: LogicalCondition;
|
|
103
|
+
/** Narrow to the rows reachable from a parent through a relation. */
|
|
104
|
+
relatedTo?: NestedPathHop;
|
|
86
105
|
}): Promise<Record<string, unknown>[]>;
|
|
87
106
|
/**
|
|
88
107
|
* Fallback path used when db.query is unavailable.
|
|
@@ -118,10 +137,6 @@ export declare class FetchService {
|
|
|
118
137
|
limit?: number;
|
|
119
138
|
databaseId?: string;
|
|
120
139
|
}): Promise<Record<string, unknown>[]>;
|
|
121
|
-
/**
|
|
122
|
-
* Fetch collection from multi-segment path
|
|
123
|
-
*/
|
|
124
|
-
private fetchCollectionFromPath;
|
|
125
140
|
/**
|
|
126
141
|
* Count rows in a collection
|
|
127
142
|
*/
|
|
@@ -130,10 +145,6 @@ export declare class FetchService {
|
|
|
130
145
|
searchString?: string;
|
|
131
146
|
databaseId?: string;
|
|
132
147
|
}): Promise<number>;
|
|
133
|
-
/**
|
|
134
|
-
* Count rows from multi-segment path
|
|
135
|
-
*/
|
|
136
|
-
private countEntitiesFromPath;
|
|
137
148
|
/**
|
|
138
149
|
* Check if a field value is unique
|
|
139
150
|
*/
|
|
@@ -160,6 +171,8 @@ export declare class FetchService {
|
|
|
160
171
|
searchString?: string;
|
|
161
172
|
databaseId?: string;
|
|
162
173
|
vectorSearch?: VectorSearchParams;
|
|
174
|
+
/** Narrow to the rows reachable from a parent through a relation. */
|
|
175
|
+
relatedTo?: NestedPathHop;
|
|
163
176
|
}, include?: string[]): Promise<Record<string, unknown>[]>;
|
|
164
177
|
/**
|
|
165
178
|
* Fetch a single row with optional relation includes for REST API.
|
|
@@ -38,6 +38,18 @@ export declare class PersistService {
|
|
|
38
38
|
* Delete all rows from a collection
|
|
39
39
|
*/
|
|
40
40
|
deleteAll(collectionPath: string, _databaseId?: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* The column on the *target* table that records the parent, for a create
|
|
43
|
+
* under a nested one-to-many path.
|
|
44
|
+
*
|
|
45
|
+
* Returns `undefined` when the link is not a column at all (a multi-hop
|
|
46
|
+
* `joinPath`), so the caller writes the row without stamping anything.
|
|
47
|
+
*
|
|
48
|
+
* `relation.localKey` is deliberately not consulted: it names a column on
|
|
49
|
+
* the *source* table. Falling back to it here — which is what this used to
|
|
50
|
+
* do, and first — stamped the parent's own foreign key onto the child row.
|
|
51
|
+
*/
|
|
52
|
+
private resolveParentForeignKeyColumn;
|
|
41
53
|
/**
|
|
42
54
|
* Save an row (create or update)
|
|
43
55
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DrizzleClient } from "../interfaces";
|
|
2
2
|
import { CollectionConfig, FilterValues, Relation } from "@rebasepro/types";
|
|
3
3
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
4
|
+
import type { NestedPathHop } from "./nested-path";
|
|
4
5
|
/**
|
|
5
6
|
* Service for handling all relation-related operations.
|
|
6
7
|
* Handles fetching, updating, and managing row relations.
|
|
@@ -85,6 +86,35 @@ export declare class RelationService {
|
|
|
85
86
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
86
87
|
databaseId?: string;
|
|
87
88
|
}): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Count the target rows a parent reaches through `relation`, narrowed by
|
|
91
|
+
* `additionalFilters` (conditions on the target table).
|
|
92
|
+
*
|
|
93
|
+
* Shared by the public count and by {@link isRelated}, so "how many children
|
|
94
|
+
* does this parent have" and "is this row one of them" are answered by the
|
|
95
|
+
* same join — a membership test that reconstructed the join separately would
|
|
96
|
+
* be free to disagree with the listing it is supposed to gate.
|
|
97
|
+
*/
|
|
98
|
+
private countRelatedRows;
|
|
99
|
+
/**
|
|
100
|
+
* Whether `targetId` is actually reachable from the parent named in `hop`.
|
|
101
|
+
*
|
|
102
|
+
* A nested address like `authors/1/posts/43` used to resolve to the target
|
|
103
|
+
* collection and then match on the primary key alone, so the parent segment
|
|
104
|
+
* decided nothing: the row came back, and was updated or deleted, whoever it
|
|
105
|
+
* belonged to. Reads, updates and deletes now all gate on this.
|
|
106
|
+
*/
|
|
107
|
+
isRelated(hop: NestedPathHop, targetId: string | number): Promise<boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* Remove the junction row linking a parent to `targetId`, leaving the target
|
|
110
|
+
* row itself alone.
|
|
111
|
+
*
|
|
112
|
+
* This is what `DELETE authors/1/tags/5` has to mean for a many-to-many: the
|
|
113
|
+
* target is shared, so deleting the row would remove the tag from every other
|
|
114
|
+
* post that uses it. It used to do exactly that — resolve the path to the
|
|
115
|
+
* `tags` table and delete by primary key.
|
|
116
|
+
*/
|
|
117
|
+
unlinkRelatedEntity(tx: DrizzleClient, hop: NestedPathHop, targetId: string | number): Promise<void>;
|
|
88
118
|
/**
|
|
89
119
|
* Batch fetch related rows for multiple parent rows to avoid N+1 queries
|
|
90
120
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CollectionConfig, Relation } from "@rebasepro/types";
|
|
2
|
+
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
3
|
+
/**
|
|
4
|
+
* The last hop of a nested collection path, e.g. `authors/1/posts`.
|
|
5
|
+
*
|
|
6
|
+
* The walk that produces this was written out four separate times — in
|
|
7
|
+
* `FetchService.fetchCollectionFromPath`, `FetchService.countEntitiesFromPath`,
|
|
8
|
+
* `PersistService.save` and `CollectionRegistry.getCollectionByPath` — and had
|
|
9
|
+
* drifted, so the read path and the write path did not agree on which relation
|
|
10
|
+
* a path named. It lives here once now.
|
|
11
|
+
*/
|
|
12
|
+
export interface NestedPathHop {
|
|
13
|
+
/** The collection the final relation is declared on (e.g. `authors`). */
|
|
14
|
+
parentCollection: CollectionConfig;
|
|
15
|
+
/** The parent's id as it appeared in the path, unparsed. */
|
|
16
|
+
parentId: string;
|
|
17
|
+
/** The path segment that named the relation (e.g. `posts`). */
|
|
18
|
+
relationKey: string;
|
|
19
|
+
relation: Relation;
|
|
20
|
+
/** `relation.target()`, resolved once. */
|
|
21
|
+
targetCollection: CollectionConfig;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True when `path` addresses rows through a relation rather than a root
|
|
25
|
+
* collection.
|
|
26
|
+
*
|
|
27
|
+
* Any separator at all counts — a root collection slug never contains one — so
|
|
28
|
+
* a malformed path like `collection/id` is a *broken* nested path and gets
|
|
29
|
+
* reported as one by {@link resolveNestedPath}, rather than being looked up as
|
|
30
|
+
* a root collection whose slug happens to contain a slash.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isNestedPath(path: string): boolean;
|
|
33
|
+
export declare function splitPathSegments(path: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Walk a nested collection path down to the relation it ends in.
|
|
36
|
+
*
|
|
37
|
+
* Returns `undefined` for a plain root-collection path so callers can keep the
|
|
38
|
+
* root case on its existing code path. Throws when the path is malformed, or
|
|
39
|
+
* when a segment names a relation that does not exist — the same errors the
|
|
40
|
+
* individual walks used to raise, with the available names attached.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveNestedPath(path: string, registry: PostgresCollectionRegistry): NestedPathHop | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* A relation reached through a junction table — many-to-many, or a multi-hop
|
|
45
|
+
* `joinPath`. The target row is shared with other parents, so writing "through"
|
|
46
|
+
* such a path addresses the *link*, not the row.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isJunctionBackedRelation(relation: Relation): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Reject a nested write whose final segment is a to-one relation.
|
|
51
|
+
*
|
|
52
|
+
* There is no column on the target row that records a to-one parent — the
|
|
53
|
+
* foreign key lives on the *parent* table. The write path used to fall through
|
|
54
|
+
* to `relation.localKey` here and stamp the parent's own FK column onto the
|
|
55
|
+
* target row, which either raised an opaque "column does not exist" or, when a
|
|
56
|
+
* column of that name happened to exist on the target, silently wrote the wrong
|
|
57
|
+
* one.
|
|
58
|
+
*/
|
|
59
|
+
export declare function assertWritableThrough(hop: NestedPathHop, path: string): void;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import "process";
|
|
3
|
+
__createRequire(import.meta.url);
|
|
4
|
+
//#region ../types/src/types/entities.ts
|
|
5
|
+
/**
|
|
6
|
+
* Class used to create a reference to a entity in a different path
|
|
7
|
+
*/
|
|
8
|
+
var EntityRelation = class {
|
|
9
|
+
__type = "relation";
|
|
10
|
+
/**
|
|
11
|
+
* ID of the entity
|
|
12
|
+
*/
|
|
13
|
+
id;
|
|
14
|
+
/**
|
|
15
|
+
* A string representing the path of the referenced document (relative
|
|
16
|
+
* to the root of the database).
|
|
17
|
+
*/
|
|
18
|
+
path;
|
|
19
|
+
/**
|
|
20
|
+
* Pre-fetched data payload to eliminate N+1 queries.
|
|
21
|
+
* When present, clients can use this directly instead of fetching.
|
|
22
|
+
*/
|
|
23
|
+
data;
|
|
24
|
+
constructor(id, path, data) {
|
|
25
|
+
this.id = id;
|
|
26
|
+
this.path = path;
|
|
27
|
+
this.data = data;
|
|
28
|
+
}
|
|
29
|
+
get pathWithId() {
|
|
30
|
+
return `${this.path}/${this.id}`;
|
|
31
|
+
}
|
|
32
|
+
isEntityReference() {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
isEntityRelation() {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var Vector = class {
|
|
40
|
+
value;
|
|
41
|
+
constructor(value) {
|
|
42
|
+
this.value = value;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region ../types/src/types/filter-operators.ts
|
|
47
|
+
/** Maps REST short-code operators to their canonical equivalents. */
|
|
48
|
+
var REST_TO_CANONICAL = {
|
|
49
|
+
"eq": "==",
|
|
50
|
+
"neq": "!=",
|
|
51
|
+
"gt": ">",
|
|
52
|
+
"gte": ">=",
|
|
53
|
+
"lt": "<",
|
|
54
|
+
"lte": "<=",
|
|
55
|
+
"in": "in",
|
|
56
|
+
"nin": "not-in",
|
|
57
|
+
"cs": "array-contains",
|
|
58
|
+
"csa": "array-contains-any",
|
|
59
|
+
"like": "like",
|
|
60
|
+
"ilike": "ilike",
|
|
61
|
+
"nlike": "not-like",
|
|
62
|
+
"nilike": "not-ilike",
|
|
63
|
+
"isnull": "is-null",
|
|
64
|
+
"notnull": "is-not-null"
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Operators that test for null/not-null and therefore ignore their value.
|
|
68
|
+
* Codecs normalize the value of these conditions to `null`.
|
|
69
|
+
*/
|
|
70
|
+
var NULL_OPS = new Set(["is-null", "is-not-null"]);
|
|
71
|
+
/**
|
|
72
|
+
* Every canonical operator, in a stable order. Useful for engine capability
|
|
73
|
+
* declarations ({@link DataSourceCapabilities.filterOperators}) and for
|
|
74
|
+
* building operator subsets.
|
|
75
|
+
* @group Models
|
|
76
|
+
*/
|
|
77
|
+
var ALL_WHERE_FILTER_OPS = [
|
|
78
|
+
"<",
|
|
79
|
+
"<=",
|
|
80
|
+
"==",
|
|
81
|
+
"!=",
|
|
82
|
+
">=",
|
|
83
|
+
">",
|
|
84
|
+
"in",
|
|
85
|
+
"not-in",
|
|
86
|
+
"array-contains",
|
|
87
|
+
"array-contains-any",
|
|
88
|
+
"like",
|
|
89
|
+
"ilike",
|
|
90
|
+
"not-like",
|
|
91
|
+
"not-ilike",
|
|
92
|
+
"is-null",
|
|
93
|
+
"is-not-null"
|
|
94
|
+
];
|
|
95
|
+
/** All canonical operator strings for runtime validation. */
|
|
96
|
+
var CANONICAL_OPS = new Set(ALL_WHERE_FILTER_OPS);
|
|
97
|
+
/**
|
|
98
|
+
* Resolve any operator string (canonical or REST short-code) to its
|
|
99
|
+
* canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* toCanonicalOp("==") // "=="
|
|
103
|
+
* toCanonicalOp("eq") // "=="
|
|
104
|
+
* toCanonicalOp("cs") // "array-contains"
|
|
105
|
+
* toCanonicalOp("xyz") // undefined
|
|
106
|
+
*/
|
|
107
|
+
function toCanonicalOp(op) {
|
|
108
|
+
if (CANONICAL_OPS.has(op)) return op;
|
|
109
|
+
return REST_TO_CANONICAL[op];
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region ../types/src/types/collections.ts
|
|
113
|
+
/**
|
|
114
|
+
* Type guard for PostgreSQL collections.
|
|
115
|
+
* Returns true if the collection uses the Postgres engine (or the default engine).
|
|
116
|
+
*
|
|
117
|
+
* Generic over the *input* type, and narrows by intersection rather than
|
|
118
|
+
* replacement. Narrowing to a bare `PostgresCollectionConfig` discarded whatever
|
|
119
|
+
* the caller actually had — most visibly the admin panel's view model, whose
|
|
120
|
+
* flattened presentation fields vanished the moment a collection passed through
|
|
121
|
+
* one of these guards.
|
|
122
|
+
*
|
|
123
|
+
* @group Models
|
|
124
|
+
*/
|
|
125
|
+
function isPostgresCollectionConfig(collection) {
|
|
126
|
+
return !collection.engine || collection.engine === "postgres";
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reads a collection's driver-declared subcollections thunk (the `subcollections`
|
|
130
|
+
* field) independent of engine identity, so engine-agnostic code doesn't have to
|
|
131
|
+
* type-guard against a specific driver. Returns `undefined` when the collection
|
|
132
|
+
* declares none.
|
|
133
|
+
*
|
|
134
|
+
* Pair with `getDataSourceCapabilities(engine).supportsSubcollections` to decide
|
|
135
|
+
* whether the engine honours subcollections at all before reading them.
|
|
136
|
+
* @group Models
|
|
137
|
+
*/
|
|
138
|
+
function getDeclaredSubcollections(collection) {
|
|
139
|
+
return collection.subcollections;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region ../types/src/types/policy.ts
|
|
143
|
+
/**
|
|
144
|
+
* The id a request without a logged-in user reports as `auth.uid()`.
|
|
145
|
+
*
|
|
146
|
+
* A user-context request always sets `app.uid`: blank would read back as
|
|
147
|
+
* `NULL`, and `NULL` is how the trusted server context is recognised, so an
|
|
148
|
+
* anonymous visitor would be promoted to server privileges. The driver
|
|
149
|
+
* therefore substitutes this sentinel at the single chokepoint where the GUC
|
|
150
|
+
* is set.
|
|
151
|
+
*
|
|
152
|
+
* The consequence for policy authors is that **`auth.uid() IS NOT NULL` is a
|
|
153
|
+
* tautology on the user path** — it is true for anonymous visitors too. Use
|
|
154
|
+
* {@link policy.authenticated} (or `auth.uid() <> 'anonymous'`) to mean "signed
|
|
155
|
+
* in", and {@link policy.serverContext} to mean "the trusted server context".
|
|
156
|
+
*
|
|
157
|
+
* @group Models
|
|
158
|
+
*/
|
|
159
|
+
var ANONYMOUS_USER_ID = "anonymous";
|
|
160
|
+
/** @group Models */
|
|
161
|
+
var policy = {
|
|
162
|
+
true: () => ({ kind: "true" }),
|
|
163
|
+
false: () => ({ kind: "false" }),
|
|
164
|
+
and: (...operands) => ({
|
|
165
|
+
kind: "and",
|
|
166
|
+
operands
|
|
167
|
+
}),
|
|
168
|
+
or: (...operands) => ({
|
|
169
|
+
kind: "or",
|
|
170
|
+
operands
|
|
171
|
+
}),
|
|
172
|
+
not: (operand) => ({
|
|
173
|
+
kind: "not",
|
|
174
|
+
operand
|
|
175
|
+
}),
|
|
176
|
+
compare: (left, op, right) => ({
|
|
177
|
+
kind: "compare",
|
|
178
|
+
op,
|
|
179
|
+
left,
|
|
180
|
+
right
|
|
181
|
+
}),
|
|
182
|
+
rolesOverlap: (roles) => ({
|
|
183
|
+
kind: "rolesOverlap",
|
|
184
|
+
roles
|
|
185
|
+
}),
|
|
186
|
+
rolesContain: (roles) => ({
|
|
187
|
+
kind: "rolesContain",
|
|
188
|
+
roles
|
|
189
|
+
}),
|
|
190
|
+
authenticated: () => ({ kind: "authenticated" }),
|
|
191
|
+
serverContext: () => ({ kind: "serverContext" }),
|
|
192
|
+
existsIn: (args) => ({
|
|
193
|
+
kind: "existsIn",
|
|
194
|
+
collection: args.collection,
|
|
195
|
+
where: args.where
|
|
196
|
+
}),
|
|
197
|
+
raw: (sql) => ({
|
|
198
|
+
kind: "raw",
|
|
199
|
+
sql
|
|
200
|
+
}),
|
|
201
|
+
field: (name) => ({
|
|
202
|
+
kind: "field",
|
|
203
|
+
name
|
|
204
|
+
}),
|
|
205
|
+
outerField: (name) => ({
|
|
206
|
+
kind: "outerField",
|
|
207
|
+
name
|
|
208
|
+
}),
|
|
209
|
+
literal: (value) => ({
|
|
210
|
+
kind: "literal",
|
|
211
|
+
value
|
|
212
|
+
}),
|
|
213
|
+
authUid: () => ({ kind: "authUid" }),
|
|
214
|
+
authRoles: () => ({ kind: "authRoles" })
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region ../types/src/types/backend.ts
|
|
218
|
+
/**
|
|
219
|
+
* Type guard: does this admin support SQL operations?
|
|
220
|
+
* @group Admin
|
|
221
|
+
*/
|
|
222
|
+
function isSQLAdmin(admin) {
|
|
223
|
+
return !!admin && typeof admin.executeSql === "function";
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Type guard: does this admin support schema management?
|
|
227
|
+
* @group Admin
|
|
228
|
+
*/
|
|
229
|
+
function isSchemaAdmin(admin) {
|
|
230
|
+
return !!admin && (typeof admin.fetchUnmappedTables === "function" || typeof admin.fetchTableMetadata === "function");
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region ../types/src/types/channel_bus.ts
|
|
234
|
+
/**
|
|
235
|
+
* Whether `setting` is an already-constructed transport rather than a request
|
|
236
|
+
* for a built-in one.
|
|
237
|
+
*
|
|
238
|
+
* Structural rather than nominal so that an instance from a *different copy* of
|
|
239
|
+
* `@rebasepro/types` — an entirely normal outcome of a separately versioned
|
|
240
|
+
* transport package — is still recognised.
|
|
241
|
+
*/
|
|
242
|
+
function isChannelBusInstance(setting) {
|
|
243
|
+
return typeof setting?.publish === "function";
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region ../types/src/types/data_source.ts
|
|
247
|
+
/**
|
|
248
|
+
* The default data-source key, used when a collection does not name a
|
|
249
|
+
* `dataSource`. Shared by the frontend router and the backend driver
|
|
250
|
+
* registry so both agree on "the default database".
|
|
251
|
+
* @group Models
|
|
252
|
+
*/
|
|
253
|
+
var DEFAULT_DATA_SOURCE_KEY = "(default)";
|
|
254
|
+
/** @group Models */
|
|
255
|
+
var POSTGRES_CAPABILITIES = {
|
|
256
|
+
key: "postgres",
|
|
257
|
+
label: "PostgreSQL",
|
|
258
|
+
supportsRelations: true,
|
|
259
|
+
supportsSubcollections: false,
|
|
260
|
+
supportsRLS: true,
|
|
261
|
+
supportsReferences: false,
|
|
262
|
+
supportsColumnTypes: true,
|
|
263
|
+
supportsRealtime: true,
|
|
264
|
+
filterOperators: ALL_WHERE_FILTER_OPS,
|
|
265
|
+
supportsSQLAdmin: true,
|
|
266
|
+
supportsDocumentAdmin: false,
|
|
267
|
+
supportsSchemaAdmin: true
|
|
268
|
+
};
|
|
269
|
+
/** @group Models */
|
|
270
|
+
var FIREBASE_CAPABILITIES = {
|
|
271
|
+
key: "firestore",
|
|
272
|
+
label: "Firebase / Firestore",
|
|
273
|
+
supportsRelations: false,
|
|
274
|
+
supportsSubcollections: true,
|
|
275
|
+
supportsRLS: false,
|
|
276
|
+
supportsReferences: true,
|
|
277
|
+
supportsColumnTypes: false,
|
|
278
|
+
supportsRealtime: true,
|
|
279
|
+
filterOperators: ALL_WHERE_FILTER_OPS.filter((op) => op !== "like" && op !== "ilike" && op !== "not-like" && op !== "not-ilike"),
|
|
280
|
+
supportsSQLAdmin: false,
|
|
281
|
+
supportsDocumentAdmin: false,
|
|
282
|
+
supportsSchemaAdmin: false
|
|
283
|
+
};
|
|
284
|
+
/** @group Models */
|
|
285
|
+
var MONGODB_CAPABILITIES = {
|
|
286
|
+
key: "mongodb",
|
|
287
|
+
label: "MongoDB",
|
|
288
|
+
supportsRelations: false,
|
|
289
|
+
supportsSubcollections: true,
|
|
290
|
+
supportsRLS: false,
|
|
291
|
+
supportsReferences: true,
|
|
292
|
+
supportsColumnTypes: false,
|
|
293
|
+
supportsRealtime: false,
|
|
294
|
+
filterOperators: ALL_WHERE_FILTER_OPS,
|
|
295
|
+
supportsSQLAdmin: false,
|
|
296
|
+
supportsDocumentAdmin: true,
|
|
297
|
+
supportsSchemaAdmin: true
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Fallback capabilities when the driver is unknown.
|
|
301
|
+
* Enables everything so nothing is hidden unexpectedly.
|
|
302
|
+
* @group Models
|
|
303
|
+
*/
|
|
304
|
+
var DEFAULT_CAPABILITIES = {
|
|
305
|
+
key: "(default)",
|
|
306
|
+
label: "Default",
|
|
307
|
+
supportsRelations: true,
|
|
308
|
+
supportsSubcollections: true,
|
|
309
|
+
supportsRLS: true,
|
|
310
|
+
supportsReferences: true,
|
|
311
|
+
supportsColumnTypes: true,
|
|
312
|
+
supportsRealtime: true,
|
|
313
|
+
filterOperators: ALL_WHERE_FILTER_OPS,
|
|
314
|
+
supportsSQLAdmin: true,
|
|
315
|
+
supportsDocumentAdmin: true,
|
|
316
|
+
supportsSchemaAdmin: true
|
|
317
|
+
};
|
|
318
|
+
var CAPABILITIES_REGISTRY = {
|
|
319
|
+
postgres: POSTGRES_CAPABILITIES,
|
|
320
|
+
firestore: FIREBASE_CAPABILITIES,
|
|
321
|
+
mongodb: MONGODB_CAPABILITIES,
|
|
322
|
+
"(default)": DEFAULT_CAPABILITIES
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Look up capabilities for a given engine key.
|
|
326
|
+
* If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.
|
|
327
|
+
* @group Models
|
|
328
|
+
*/
|
|
329
|
+
function getDataSourceCapabilities(engine) {
|
|
330
|
+
if (!engine) return POSTGRES_CAPABILITIES;
|
|
331
|
+
return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
export { isSchemaAdmin as a, getDeclaredSubcollections as c, REST_TO_CANONICAL as d, toCanonicalOp as f, isSQLAdmin as i, isPostgresCollectionConfig as l, Vector as m, getDataSourceCapabilities as n, ANONYMOUS_USER_ID as o, EntityRelation as p, isChannelBusInstance as r, policy as s, DEFAULT_DATA_SOURCE_KEY as t, NULL_OPS as u };
|
|
335
|
+
|
|
336
|
+
//# sourceMappingURL=src-CBgtrPhJ.js.map
|