@rebasepro/common 0.1.2 → 0.2.3
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/LICENSE +1 -1
- package/dist/data/query_builder.d.ts +51 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +396 -188
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +395 -187
- package/dist/index.umd.js.map +1 -1
- package/dist/util/entities.d.ts +2 -2
- package/dist/util/relations.d.ts +1 -1
- package/package.json +16 -15
- package/src/collections/CollectionRegistry.ts +24 -37
- package/src/data/buildRebaseData.ts +25 -5
- package/src/data/query_builder.ts +117 -0
- package/src/index.ts +2 -0
- package/src/types/json-logic-js.d.ts +1 -1
- package/src/util/builders.ts +1 -0
- package/src/util/entities.ts +6 -2
- package/src/util/relations.ts +83 -22
- package/src/util/resolutions.ts +6 -3
package/dist/util/entities.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export declare function isReadOnly(property: Property): boolean;
|
|
|
3
3
|
export declare function isHidden(property: Property): boolean;
|
|
4
4
|
export declare function isPropertyBuilder(property?: Property): boolean;
|
|
5
5
|
export declare function getDefaultValuesFor<M extends Record<string, unknown>>(properties: Properties): Partial<EntityValues<M>>;
|
|
6
|
-
export declare function getDefaultValueFor(property?: Property):
|
|
7
|
-
export declare function getDefaultValueFortype(type: DataType):
|
|
6
|
+
export declare function getDefaultValueFor(property?: Property): unknown;
|
|
7
|
+
export declare function getDefaultValueFortype(type: DataType): unknown;
|
|
8
8
|
/**
|
|
9
9
|
* Update the automatic values in an entity before save
|
|
10
10
|
* @group Driver
|
package/dist/util/relations.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EntityCollection, Property, Relation } from "@rebasepro/types";
|
|
2
|
-
export declare function sanitizeRelation(relation: Partial<Relation>, sourceCollection: EntityCollection): Relation;
|
|
2
|
+
export declare function sanitizeRelation(relation: Partial<Relation>, sourceCollection: EntityCollection, resolveCollection?: (slugOrTable: string) => EntityCollection | undefined): Relation;
|
|
3
3
|
export declare function resolveCollectionRelations(collection: EntityCollection): Record<string, Relation>;
|
|
4
4
|
export declare function resolvePropertyRelation({ propertyKey, property, sourceCollection }: {
|
|
5
5
|
propertyKey: string;
|
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.2.3",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
34
|
"types": "./dist/index.d.ts",
|
|
35
|
-
"development": "./
|
|
35
|
+
"development": "./dist/index.es.js",
|
|
36
36
|
"import": "./dist/index.es.js",
|
|
37
37
|
"require": "./dist/index.umd.js"
|
|
38
38
|
},
|
|
@@ -41,29 +41,27 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"fast-equals": "6.0.0",
|
|
43
43
|
"json-logic-js": "^2.0.5",
|
|
44
|
-
"@rebasepro/types": "0.
|
|
45
|
-
"@rebasepro/utils": "0.
|
|
44
|
+
"@rebasepro/types": "0.2.3",
|
|
45
|
+
"@rebasepro/utils": "0.2.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@jest/globals": "^29.7.0",
|
|
49
|
-
"@testing-library/react": "^16.3.
|
|
49
|
+
"@testing-library/react": "^16.3.2",
|
|
50
50
|
"@testing-library/user-event": "^14.6.1",
|
|
51
51
|
"@types/jest": "^29.5.14",
|
|
52
|
-
"@types/node": "^20.
|
|
52
|
+
"@types/node": "^20.19.41",
|
|
53
53
|
"@types/object-hash": "^3.0.6",
|
|
54
|
-
"@types/react": "^19.
|
|
55
|
-
"@types/react-dom": "^19.
|
|
54
|
+
"@types/react": "^19.2.15",
|
|
55
|
+
"@types/react-dom": "^19.2.3",
|
|
56
56
|
"@types/react-measure": "^2.0.12",
|
|
57
|
-
"@vitejs/plugin-react": "^4.
|
|
57
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
58
58
|
"babel-plugin-react-compiler": "beta",
|
|
59
59
|
"cross-env": "^7.0.3",
|
|
60
|
-
"eslint-plugin-react-compiler": "beta",
|
|
61
60
|
"jest": "^29.7.0",
|
|
62
|
-
"
|
|
63
|
-
"ts-jest": "^29.3.1",
|
|
61
|
+
"ts-jest": "^29.4.10",
|
|
64
62
|
"tsd": "^0.31.2",
|
|
65
|
-
"typescript": "^5.
|
|
66
|
-
"vite": "^5.4.
|
|
63
|
+
"typescript": "^5.9.3",
|
|
64
|
+
"vite": "^5.4.21"
|
|
67
65
|
},
|
|
68
66
|
"files": [
|
|
69
67
|
"dist",
|
|
@@ -94,7 +92,10 @@
|
|
|
94
92
|
],
|
|
95
93
|
"testEnvironment": "node",
|
|
96
94
|
"moduleNameMapper": {
|
|
97
|
-
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
|
|
95
|
+
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
|
|
96
|
+
"^@rebasepro/client$": "<rootDir>/../client/src/index.ts",
|
|
97
|
+
"^@rebasepro/types$": "<rootDir>/../types/src/index.ts",
|
|
98
|
+
"^@rebasepro/utils$": "<rootDir>/../utils/src/index.ts"
|
|
98
99
|
}
|
|
99
100
|
},
|
|
100
101
|
"scripts": {
|
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
Relation,
|
|
10
10
|
RelationProperty,
|
|
11
11
|
StringProperty,
|
|
12
|
-
OnAction,
|
|
13
|
-
JoinStep,
|
|
14
12
|
getDataSourceCapabilities
|
|
15
13
|
} from "@rebasepro/types";
|
|
16
14
|
import { deepEqual } from "fast-equals";
|
|
@@ -71,8 +69,14 @@ export class CollectionRegistry {
|
|
|
71
69
|
return false;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
// Raw input has changed — normalize and register
|
|
75
72
|
this.reset();
|
|
73
|
+
// Phase 0: Populate maps with raw collections first for string target resolution
|
|
74
|
+
collections.forEach((c) => {
|
|
75
|
+
if (c.slug) {
|
|
76
|
+
this.collectionsBySlug.set(c.slug, c);
|
|
77
|
+
}
|
|
78
|
+
this.collectionsByTableName.set(getTableName(c), c);
|
|
79
|
+
});
|
|
76
80
|
|
|
77
81
|
const normalizedCollections = collections.map(c => this.normalizeCollection({ ...c }));
|
|
78
82
|
|
|
@@ -164,12 +168,23 @@ export class CollectionRegistry {
|
|
|
164
168
|
|
|
165
169
|
// 2. Merge with manual relations[] (manual entries win on name conflict)
|
|
166
170
|
const relResult = result as CollectionWithRelations;
|
|
167
|
-
const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? (
|
|
171
|
+
const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? (relResult.relations ?? []) : [];
|
|
168
172
|
const mergedRelationsRaw = [...extractedRelations];
|
|
169
173
|
for (const manual of manualRelations) {
|
|
170
174
|
const name = manual.relationName;
|
|
171
|
-
if (!name
|
|
175
|
+
if (!name) {
|
|
172
176
|
mergedRelationsRaw.push(manual);
|
|
177
|
+
} else {
|
|
178
|
+
const existingIndex = mergedRelationsRaw.findIndex(r => r.relationName === name);
|
|
179
|
+
if (existingIndex === -1) {
|
|
180
|
+
mergedRelationsRaw.push(manual);
|
|
181
|
+
} else {
|
|
182
|
+
// Merge manual into existing, preserving custom fields like 'collection'
|
|
183
|
+
mergedRelationsRaw[existingIndex] = {
|
|
184
|
+
...manual,
|
|
185
|
+
...mergedRelationsRaw[existingIndex]
|
|
186
|
+
};
|
|
187
|
+
}
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
|
|
@@ -182,7 +197,7 @@ export class CollectionRegistry {
|
|
|
182
197
|
if (getDataSourceCapabilities(result.driver).supportsRelations) {
|
|
183
198
|
mergedRelations = mergedRelationsRaw.map(r => {
|
|
184
199
|
try {
|
|
185
|
-
return sanitizeRelation(r, result);
|
|
200
|
+
return sanitizeRelation(r, result, (slug) => this.get(slug));
|
|
186
201
|
} catch {
|
|
187
202
|
// sanitizeRelation may throw for incomplete configs
|
|
188
203
|
// (e.g. missing target). Keep the raw relation as-is.
|
|
@@ -191,7 +206,7 @@ export class CollectionRegistry {
|
|
|
191
206
|
});
|
|
192
207
|
|
|
193
208
|
// 3. Set the merged relations on the result copy
|
|
194
|
-
|
|
209
|
+
relResult.relations = mergedRelations;
|
|
195
210
|
}
|
|
196
211
|
|
|
197
212
|
// 4. Normalize properties (which stamps relation on each property)
|
|
@@ -202,8 +217,8 @@ export class CollectionRegistry {
|
|
|
202
217
|
if (!result.childCollections) {
|
|
203
218
|
if (getDataSourceCapabilities(result.driver).supportsSubcollections && (result as CollectionWithSubcollections).subcollections) {
|
|
204
219
|
result.childCollections = (result as CollectionWithSubcollections).subcollections;
|
|
205
|
-
} else if (getDataSourceCapabilities(result.driver).supportsRelations &&
|
|
206
|
-
const manyRelations =
|
|
220
|
+
} else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
|
|
221
|
+
const manyRelations = relResult.relations.filter((r: Relation) => r.cardinality === "many");
|
|
207
222
|
if (manyRelations.length > 0) {
|
|
208
223
|
result.childCollections = () => manyRelations.map((r: Relation) => {
|
|
209
224
|
const target = r.target();
|
|
@@ -463,31 +478,3 @@ export class CollectionRegistry {
|
|
|
463
478
|
|
|
464
479
|
}
|
|
465
480
|
|
|
466
|
-
function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
|
|
467
|
-
// console.log("Comparing collection lists", a, b);
|
|
468
|
-
// return true;
|
|
469
|
-
if (a.length !== b.length) {
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
const aCopy = [...a];
|
|
473
|
-
const bCopy = [...b];
|
|
474
|
-
const aSorted = aCopy.sort((x, y) => x.slug.localeCompare(y.slug));
|
|
475
|
-
const bSorted = bCopy.sort((x, y) => x.slug.localeCompare(y.slug));
|
|
476
|
-
return aSorted.every((value, index) => areCollectionsEqual(value, bSorted[index]));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function areCollectionsEqual(a: EntityCollection, b: EntityCollection) {
|
|
480
|
-
const subcollectionsA = getSubcollections(a);
|
|
481
|
-
const subcollectionsB = getSubcollections(b);
|
|
482
|
-
const { driver: _dA, ...restA } = a as EntityCollection & Record<string, unknown>;
|
|
483
|
-
const { driver: _dB, ...restB } = b as EntityCollection & Record<string, unknown>;
|
|
484
|
-
// Remove subcollections/relations from comparison objects (already handled above)
|
|
485
|
-
delete restA.subcollections;
|
|
486
|
-
delete restB.subcollections;
|
|
487
|
-
delete (restA as any).relations;
|
|
488
|
-
delete (restB as any).relations;
|
|
489
|
-
if (!areCollectionListsEqual(subcollectionsA, subcollectionsB)) {
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
return deepEqual(removeFunctions(restA), removeFunctions(restB));
|
|
493
|
-
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
WhereFieldValue
|
|
12
12
|
} from "@rebasepro/types";
|
|
13
13
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
14
|
+
import { QueryBuilder } from "./query_builder";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Convert where-clause filter object to the internal DataDriver FilterValues format.
|
|
@@ -129,14 +130,11 @@ function parseOrderBy(orderBy?: string): [string, "asc" | "desc"] | undefined {
|
|
|
129
130
|
return [field, direction];
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
/**
|
|
133
|
-
* Create a CollectionAccessor that delegates to a DataDriver for a given collection slug.
|
|
134
|
-
*/
|
|
135
133
|
function createDriverAccessor<M extends Record<string, unknown> = Record<string, unknown>>(
|
|
136
134
|
driver: DataDriver,
|
|
137
135
|
slug: string
|
|
138
136
|
): CollectionAccessor<M> {
|
|
139
|
-
|
|
137
|
+
const accessor: CollectionAccessor<M> = {
|
|
140
138
|
async find(params?: FindParams): Promise<FindResponse<M>> {
|
|
141
139
|
const orderParsed = parseOrderBy(params?.orderBy);
|
|
142
140
|
const entities = await driver.fetchCollection<M>({
|
|
@@ -237,8 +235,30 @@ values: {} as Record<string, unknown> }
|
|
|
237
235
|
onUpdate: (entity) => onUpdate(entity ?? undefined),
|
|
238
236
|
onError
|
|
239
237
|
});
|
|
240
|
-
} : undefined
|
|
238
|
+
} : undefined,
|
|
239
|
+
|
|
240
|
+
// Fluent Query Builder
|
|
241
|
+
where(column: keyof M & string, operator: any, value: unknown) {
|
|
242
|
+
return new QueryBuilder<M>(accessor).where(column, operator, value);
|
|
243
|
+
},
|
|
244
|
+
orderBy(column: keyof M & string, ascending?: "asc" | "desc") {
|
|
245
|
+
return new QueryBuilder<M>(accessor).orderBy(column, ascending);
|
|
246
|
+
},
|
|
247
|
+
limit(count: number) {
|
|
248
|
+
return new QueryBuilder<M>(accessor).limit(count);
|
|
249
|
+
},
|
|
250
|
+
offset(count: number) {
|
|
251
|
+
return new QueryBuilder<M>(accessor).offset(count);
|
|
252
|
+
},
|
|
253
|
+
search(searchString: string) {
|
|
254
|
+
return new QueryBuilder<M>(accessor).search(searchString);
|
|
255
|
+
},
|
|
256
|
+
include(...relations: string[]) {
|
|
257
|
+
return new QueryBuilder<M>(accessor).include(...relations);
|
|
258
|
+
}
|
|
241
259
|
};
|
|
260
|
+
|
|
261
|
+
return accessor;
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
/**
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { FindParams, Entity, FindResponse, CollectionAccessor, QueryBuilderInterface, FilterOperator } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps standard operators to Rebase backend's string-based operators
|
|
5
|
+
*/
|
|
6
|
+
function mapOperator(op: FilterOperator): string {
|
|
7
|
+
switch (op) {
|
|
8
|
+
case "==": return "eq";
|
|
9
|
+
case "!=": return "neq";
|
|
10
|
+
case ">": return "gt";
|
|
11
|
+
case ">=": return "gte";
|
|
12
|
+
case "<": return "lt";
|
|
13
|
+
case "<=": return "lte";
|
|
14
|
+
case "array-contains": return "cs";
|
|
15
|
+
case "array-contains-any": return "csa";
|
|
16
|
+
case "not-in": return "nin";
|
|
17
|
+
default: return op;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {
|
|
22
|
+
private params: FindParams = { where: {} };
|
|
23
|
+
|
|
24
|
+
constructor(private collection: CollectionAccessor<M>) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Add a filter condition to your query.
|
|
28
|
+
* @example
|
|
29
|
+
* client.collection('users').where('age', '>=', 18).find()
|
|
30
|
+
*/
|
|
31
|
+
where(column: keyof M & string, operator: FilterOperator, value: unknown): this {
|
|
32
|
+
if (!this.params.where) {
|
|
33
|
+
this.params.where = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mappedOp = mapOperator(operator);
|
|
37
|
+
let formattedValue = value;
|
|
38
|
+
|
|
39
|
+
// Handle arrays for in, nin, cs, csa
|
|
40
|
+
if (Array.isArray(value) && ["in", "nin", "cs", "csa"].includes(mappedOp)) {
|
|
41
|
+
formattedValue = `(${value.join(",")})`;
|
|
42
|
+
} else if (value === null) {
|
|
43
|
+
formattedValue = "null";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.params.where[column] = mappedOp === "eq" ? String(formattedValue) : `${mappedOp}.${formattedValue}`;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Order the results by a specific column.
|
|
52
|
+
* @example
|
|
53
|
+
* client.collection('users').orderBy('createdAt', 'desc').find()
|
|
54
|
+
*/
|
|
55
|
+
orderBy(column: keyof M & string, ascending: "asc" | "desc" = "asc"): this {
|
|
56
|
+
this.params.orderBy = `${column}:${ascending}`;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Limit the number of results returned.
|
|
62
|
+
*/
|
|
63
|
+
limit(count: number): this {
|
|
64
|
+
this.params.limit = count;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Skip the first N results.
|
|
70
|
+
*/
|
|
71
|
+
offset(count: number): this {
|
|
72
|
+
this.params.offset = count;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set a free-text search string if supported by the backend.
|
|
78
|
+
*/
|
|
79
|
+
search(searchString: string): this {
|
|
80
|
+
this.params.searchString = searchString;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Include related entities in the response.
|
|
86
|
+
* Relations will be populated with full entity data instead of just IDs.
|
|
87
|
+
*
|
|
88
|
+
* @param relations - Relation names to include, or "*" for all.
|
|
89
|
+
* @example
|
|
90
|
+
* // Include specific relations
|
|
91
|
+
* client.data.posts.include("tags", "author").find()
|
|
92
|
+
*
|
|
93
|
+
* // Include all relations
|
|
94
|
+
* client.data.posts.include("*").find()
|
|
95
|
+
*/
|
|
96
|
+
include(...relations: string[]): this {
|
|
97
|
+
this.params.include = relations;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Execute the find query and return the results.
|
|
103
|
+
*/
|
|
104
|
+
async find(): Promise<FindResponse<M>> {
|
|
105
|
+
return this.collection.find(this.params) as Promise<FindResponse<M>>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Listen to realtime updates matching this query.
|
|
110
|
+
*/
|
|
111
|
+
listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void {
|
|
112
|
+
if (!this.collection.listen) {
|
|
113
|
+
throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
|
|
114
|
+
}
|
|
115
|
+
return this.collection.listen(this.params, onUpdate, onError);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare module "json-logic-js" {
|
|
2
2
|
interface JsonLogic {
|
|
3
3
|
apply(logic: unknown, data?: unknown): unknown;
|
|
4
|
-
add_operation<T extends
|
|
4
|
+
add_operation<T extends unknown[]>(name: string, fn: (...args: T) => unknown): void;
|
|
5
5
|
}
|
|
6
6
|
const jsonLogic: JsonLogic;
|
|
7
7
|
export default jsonLogic;
|
package/src/util/builders.ts
CHANGED
|
@@ -50,6 +50,7 @@ export function buildProperty<T, P extends Property = Property>(
|
|
|
50
50
|
P extends ArrayProperty ? ArrayProperty :
|
|
51
51
|
P extends MapProperty ? MapProperty : never {
|
|
52
52
|
|
|
53
|
+
// SAFETY: Identity function — P is a subtype of the conditional return type by definition
|
|
53
54
|
return property as unknown as ReturnType<typeof buildProperty<T, P>>;
|
|
54
55
|
}
|
|
55
56
|
|
package/src/util/entities.ts
CHANGED
|
@@ -44,7 +44,7 @@ export function getDefaultValuesFor<M extends Record<string, unknown>>(propertie
|
|
|
44
44
|
...b }), {}) as EntityValues<M>;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function getDefaultValueFor(property?: Property) {
|
|
47
|
+
export function getDefaultValueFor(property?: Property): unknown {
|
|
48
48
|
if (!property) return undefined;
|
|
49
49
|
if (isPropertyBuilder(property)) return undefined;
|
|
50
50
|
if (property.defaultValue || property.defaultValue === null) {
|
|
@@ -58,7 +58,7 @@ export function getDefaultValueFor(property?: Property) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function getDefaultValueFortype(type: DataType) {
|
|
61
|
+
export function getDefaultValueFortype(type: DataType): unknown {
|
|
62
62
|
if (type === "string") {
|
|
63
63
|
return null;
|
|
64
64
|
} else if (type === "number") {
|
|
@@ -71,6 +71,10 @@ export function getDefaultValueFortype(type: DataType) {
|
|
|
71
71
|
return [];
|
|
72
72
|
} else if (type === "map") {
|
|
73
73
|
return {};
|
|
74
|
+
} else if (type === "vector") {
|
|
75
|
+
return null;
|
|
76
|
+
} else if (type === "binary") {
|
|
77
|
+
return null;
|
|
74
78
|
} else {
|
|
75
79
|
return null;
|
|
76
80
|
}
|
package/src/util/relations.ts
CHANGED
|
@@ -2,14 +2,60 @@ import { CollectionWithRelations, EntityCollection, getDataSourceCapabilities, P
|
|
|
2
2
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
3
3
|
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
4
4
|
|
|
5
|
-
export function sanitizeRelation(
|
|
5
|
+
export function sanitizeRelation(
|
|
6
|
+
relation: Partial<Relation>,
|
|
7
|
+
sourceCollection: EntityCollection,
|
|
8
|
+
resolveCollection?: (slugOrTable: string) => EntityCollection | undefined
|
|
9
|
+
): Relation {
|
|
6
10
|
if (!relation.target) {
|
|
7
11
|
throw new Error("Relation is missing a `target` collection.");
|
|
8
12
|
}
|
|
9
|
-
|
|
13
|
+
|
|
14
|
+
const rawTarget = relation.target;
|
|
15
|
+
let targetCollection: EntityCollection | undefined;
|
|
16
|
+
|
|
17
|
+
if (typeof rawTarget === "string") {
|
|
18
|
+
if (resolveCollection) {
|
|
19
|
+
targetCollection = resolveCollection(rawTarget);
|
|
20
|
+
}
|
|
21
|
+
if (!targetCollection) {
|
|
22
|
+
targetCollection = { slug: rawTarget, name: rawTarget } as EntityCollection;
|
|
23
|
+
}
|
|
24
|
+
} else if (typeof rawTarget === "function") {
|
|
25
|
+
const evaluated = rawTarget();
|
|
26
|
+
if (typeof evaluated === "string") {
|
|
27
|
+
if (resolveCollection) {
|
|
28
|
+
targetCollection = resolveCollection(evaluated);
|
|
29
|
+
}
|
|
30
|
+
if (!targetCollection) {
|
|
31
|
+
targetCollection = { slug: evaluated, name: evaluated } as EntityCollection;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
targetCollection = evaluated;
|
|
35
|
+
}
|
|
36
|
+
} else if (rawTarget && typeof rawTarget === "object") {
|
|
37
|
+
targetCollection = rawTarget as EntityCollection;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!targetCollection) {
|
|
41
|
+
throw new Error("Relation is missing a valid `target` collection.");
|
|
42
|
+
}
|
|
10
43
|
|
|
11
44
|
const newRelation: Partial<Relation> = { ...relation };
|
|
12
45
|
|
|
46
|
+
newRelation.target = () => {
|
|
47
|
+
if (typeof rawTarget === "string") {
|
|
48
|
+
return (resolveCollection && resolveCollection(rawTarget)) || targetCollection!;
|
|
49
|
+
} else if (typeof rawTarget === "function") {
|
|
50
|
+
const evaluated = rawTarget();
|
|
51
|
+
if (typeof evaluated === "string") {
|
|
52
|
+
return (resolveCollection && resolveCollection(evaluated)) || targetCollection!;
|
|
53
|
+
}
|
|
54
|
+
return evaluated;
|
|
55
|
+
}
|
|
56
|
+
return targetCollection!;
|
|
57
|
+
};
|
|
58
|
+
|
|
13
59
|
// 1. Default relationName from target collection slug
|
|
14
60
|
if (!newRelation.relationName) {
|
|
15
61
|
newRelation.relationName = toSnakeCase(targetCollection.slug);
|
|
@@ -41,7 +87,7 @@ export function sanitizeRelation(relation: Partial<Relation>, sourceCollection:
|
|
|
41
87
|
|
|
42
88
|
try {
|
|
43
89
|
// Look for an owning relation on the target that points back to this collection
|
|
44
|
-
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? (((targetCollection as
|
|
90
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? (((targetCollection as CollectionWithRelations).relations) || []) : [];
|
|
45
91
|
for (const targetRel of targetRelations) {
|
|
46
92
|
if (targetRel.direction === "owning" &&
|
|
47
93
|
targetRel.cardinality === "one" &&
|
|
@@ -85,16 +131,33 @@ export function sanitizeRelation(relation: Partial<Relation>, sourceCollection:
|
|
|
85
131
|
// Note: we intentionally do NOT require `through` here because the raw (unsanitized)
|
|
86
132
|
// relations won't have `through` populated yet — sanitizeRelation fills it in later.
|
|
87
133
|
// `cardinality: "many" + direction: "owning"` is sufficient to identify owning M2M.
|
|
88
|
-
|
|
134
|
+
|
|
135
|
+
// 1. Check the explicit relations[] array
|
|
136
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? (((targetCollection as CollectionWithRelations).relations) || []) : [];
|
|
89
137
|
for (const targetRel of targetRelations) {
|
|
90
138
|
if (targetRel.cardinality === "many" &&
|
|
91
139
|
(targetRel.direction === "owning" || !targetRel.direction) &&
|
|
92
140
|
(targetRel.relationName === newRelation.inverseRelationName)) {
|
|
93
|
-
// Found a corresponding owning many-to-many relation
|
|
94
141
|
isManyToManyInverse = true;
|
|
95
142
|
break;
|
|
96
143
|
}
|
|
97
144
|
}
|
|
145
|
+
|
|
146
|
+
// 2. Also check the target's properties for inline relation definitions
|
|
147
|
+
// (e.g. posts.properties.tags = { type: "relation", cardinality: "many", direction: "owning" })
|
|
148
|
+
if (!isManyToManyInverse && targetCollection.properties) {
|
|
149
|
+
for (const [propKey, prop] of Object.entries(targetCollection.properties)) {
|
|
150
|
+
if ((prop as Property).type !== "relation") continue;
|
|
151
|
+
const relProp = prop as RelationProperty;
|
|
152
|
+
const relName = relProp.relationName || propKey;
|
|
153
|
+
if (relName === newRelation.inverseRelationName &&
|
|
154
|
+
relProp.cardinality === "many" &&
|
|
155
|
+
(relProp.direction === "owning" || !relProp.direction)) {
|
|
156
|
+
isManyToManyInverse = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
98
161
|
} catch (e) {
|
|
99
162
|
// If we can't inspect the target collection, assume one-to-many
|
|
100
163
|
}
|
|
@@ -152,13 +215,17 @@ export function resolveCollectionRelations(
|
|
|
152
215
|
|
|
153
216
|
// 1. Process explicit relations from the `relations` field.
|
|
154
217
|
// Each relation is stored once under its canonical relationName key.
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
218
|
+
if (relCollection.relations) {
|
|
219
|
+
relCollection.relations.forEach((relation: Relation) => {
|
|
220
|
+
try {
|
|
221
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
222
|
+
const relationKey = normalizedRelation.relationName;
|
|
223
|
+
if (relationKey) {
|
|
224
|
+
relations[relationKey] = normalizedRelation;
|
|
225
|
+
registeredRelationNames.add(relationKey);
|
|
226
|
+
}
|
|
227
|
+
} catch (e) {
|
|
228
|
+
// Ignore incomplete or invalid relations (e.g. missing target during registry setup)
|
|
162
229
|
}
|
|
163
230
|
});
|
|
164
231
|
}
|
|
@@ -213,7 +280,8 @@ export function resolvePropertyRelation({
|
|
|
213
280
|
|
|
214
281
|
const relProp = property as RelationProperty;
|
|
215
282
|
|
|
216
|
-
//
|
|
283
|
+
// If the property has inline config (target set), build a Relation from it.
|
|
284
|
+
// We only support the flat format where properties are directly on the RelationProperty.
|
|
217
285
|
if (relProp.target) {
|
|
218
286
|
return {
|
|
219
287
|
relationName: relProp.relationName || propertyKey,
|
|
@@ -231,15 +299,8 @@ export function resolvePropertyRelation({
|
|
|
231
299
|
} as Relation;
|
|
232
300
|
}
|
|
233
301
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!relation) {
|
|
237
|
-
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return relation as Relation;
|
|
242
|
-
|
|
302
|
+
console.warn(`Unrecognized or missing relation target for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
303
|
+
return undefined;
|
|
243
304
|
}
|
|
244
305
|
|
|
245
306
|
export function getTableName(collection: EntityCollection): string {
|
package/src/util/resolutions.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { enumToObjectEntries } from "./enums";
|
|
|
21
21
|
import { DEFAULT_ONE_OF_TYPE } from "./common";
|
|
22
22
|
import { isDefaultFieldConfigId } from "@rebasepro/utils";
|
|
23
23
|
import { getIn, mergeDeep } from "@rebasepro/utils";
|
|
24
|
+
import { resolveCollectionRelations } from "./relations";
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Resolve property builders, enums and arrays.
|
|
@@ -350,8 +351,10 @@ export function getSubcollections<M extends Record<string, unknown> = Record<str
|
|
|
350
351
|
return (collection as CollectionWithSubcollections).subcollections!() ?? [];
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
if (getDataSourceCapabilities(collection.driver).supportsRelations
|
|
354
|
-
const
|
|
354
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
355
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
356
|
+
const manyRelations = Object.values(resolvedRelations).filter((r: Relation) => r.cardinality === "many");
|
|
357
|
+
|
|
355
358
|
return manyRelations.map((r: Relation) => {
|
|
356
359
|
const target = r.target();
|
|
357
360
|
if (!target) return undefined;
|
|
@@ -376,7 +379,7 @@ export function getSubcollections<M extends Record<string, unknown> = Record<str
|
|
|
376
379
|
|
|
377
380
|
const targetWithOverrides = { ...target, ...baseOverrides };
|
|
378
381
|
return (r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides) as EntityCollection<Record<string, unknown>>;
|
|
379
|
-
}).filter((c:
|
|
382
|
+
}).filter((c: EntityCollection<Record<string, unknown>> | undefined): c is EntityCollection<Record<string, unknown>> => Boolean(c));
|
|
380
383
|
}
|
|
381
384
|
|
|
382
385
|
return [];
|