@rebasepro/common 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/README.md +2 -2
- package/dist/collections/CollectionRegistry.d.ts +8 -0
- package/dist/index.es.js +281 -82
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +280 -81
- package/dist/index.umd.js.map +1 -1
- package/dist/util/entities.d.ts +22 -0
- package/dist/util/relations.d.ts +14 -4
- package/dist/util/resolutions.d.ts +1 -1
- package/package.json +106 -108
- package/src/collections/CollectionRegistry.ts +153 -44
- package/src/data/buildRebaseData.ts +88 -24
- package/src/util/builders.ts +1 -1
- package/src/util/callbacks.ts +3 -2
- package/src/util/collections.ts +9 -6
- package/src/util/conditions.ts +2 -2
- package/src/util/entities.ts +38 -4
- package/src/util/navigation_from_path.ts +1 -1
- package/src/util/parent_references_from_path.ts +3 -2
- package/src/util/permissions.test.ts +213 -91
- package/src/util/permissions.ts +14 -14
- package/src/util/relations.ts +97 -21
- package/src/util/resolutions.ts +69 -38
package/README.md
CHANGED
|
@@ -69,9 +69,9 @@ Rebase has been meticulously crafted to make it incredibly easy for developers
|
|
|
69
69
|
to build a CMS/admin tool while offering an excellent data editing experience
|
|
70
70
|
and a user-friendly interface for marketers and content managers.
|
|
71
71
|
|
|
72
|
-
### 🏓 Exceptional
|
|
72
|
+
### 🏓 Exceptional Table View
|
|
73
73
|
|
|
74
|
-
We've developed a highly efficient windowed **
|
|
74
|
+
We've developed a highly efficient windowed **table view** for
|
|
75
75
|
collections, allowing inline editing for most common fields, as well as popup
|
|
76
76
|
views for other cases and your custom field implementations.
|
|
77
77
|
|
|
@@ -3,9 +3,11 @@ export declare class CollectionRegistry {
|
|
|
3
3
|
private collectionsByTableName;
|
|
4
4
|
private collectionsBySlug;
|
|
5
5
|
private rootCollections;
|
|
6
|
+
private cachedCollectionsList;
|
|
6
7
|
private rawCollectionsByTableName;
|
|
7
8
|
private rawCollectionsBySlug;
|
|
8
9
|
private rawRootCollections;
|
|
10
|
+
private cachedRawCollectionsList;
|
|
9
11
|
private lastRawInputSnapshot;
|
|
10
12
|
constructor(collections?: EntityCollection[]);
|
|
11
13
|
reset(): void;
|
|
@@ -21,6 +23,12 @@ export declare class CollectionRegistry {
|
|
|
21
23
|
register(collection: EntityCollection, rawCollection?: EntityCollection): void;
|
|
22
24
|
private _registerRecursively;
|
|
23
25
|
normalizeCollection(collection: EntityCollection): EntityCollection;
|
|
26
|
+
/**
|
|
27
|
+
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
28
|
+
* This allows developers to define relations directly on properties without a separate
|
|
29
|
+
* `relations[]` entry on the collection.
|
|
30
|
+
*/
|
|
31
|
+
private extractRelationsFromProperties;
|
|
24
32
|
private normalizeProperties;
|
|
25
33
|
private normalizeProperty;
|
|
26
34
|
get(path: string): EntityCollection | undefined;
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityReference, EntityRelation,
|
|
1
|
+
import { EntityReference, EntityRelation, getDataSourceCapabilities } from "@rebasepro/types";
|
|
2
2
|
import { mergeDeep, getIn as getIn$1, isDefaultFieldConfigId, randomString, toSnakeCase, generateForeignKeyName, removeFunctions } from "@rebasepro/utils";
|
|
3
3
|
import jsonLogic from "json-logic-js";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
@@ -108,7 +108,7 @@ function normalizeToEntityRelation(value) {
|
|
|
108
108
|
if (value instanceof EntityRelation) return value;
|
|
109
109
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
110
110
|
const obj = value;
|
|
111
|
-
const isRelationLike = obj.__type === "relation" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation();
|
|
111
|
+
const isRelationLike = obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference();
|
|
112
112
|
if (!isRelationLike) return null;
|
|
113
113
|
return new EntityRelation(obj.id, obj.path, obj.data);
|
|
114
114
|
}
|
|
@@ -166,6 +166,21 @@ function traverseValueProperty(inputValue, property, operation) {
|
|
|
166
166
|
}
|
|
167
167
|
return value;
|
|
168
168
|
}
|
|
169
|
+
function createRelationRef(id, path) {
|
|
170
|
+
return {
|
|
171
|
+
id,
|
|
172
|
+
path,
|
|
173
|
+
__type: "relation"
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function createRelationRefWithData(id, path, data) {
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
path,
|
|
180
|
+
__type: "relation",
|
|
181
|
+
data
|
|
182
|
+
};
|
|
183
|
+
}
|
|
169
184
|
function sortProperties(properties, propertiesOrder) {
|
|
170
185
|
try {
|
|
171
186
|
const propertiesKeys = Object.keys(properties);
|
|
@@ -307,22 +322,24 @@ function resolveProperty(props) {
|
|
|
307
322
|
let resultProperty;
|
|
308
323
|
if (isPropertyBuilder(property)) {
|
|
309
324
|
const path = rest.path;
|
|
310
|
-
if (!path)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
325
|
+
if (!path) {
|
|
326
|
+
resultProperty = property;
|
|
327
|
+
} else {
|
|
328
|
+
const usedPropertyValue = rest.propertyKey ? getIn$1(rest.values, rest.propertyKey) : void 0;
|
|
329
|
+
const dynamicProps = property.dynamicProps?.({
|
|
330
|
+
...rest,
|
|
331
|
+
path,
|
|
332
|
+
propertyValue: usedPropertyValue,
|
|
333
|
+
values: rest.values ?? {},
|
|
334
|
+
previousValues: rest.previousValues ?? rest.values ?? {}
|
|
335
|
+
});
|
|
336
|
+
resultProperty = mergeDeep(property, dynamicProps ?? {});
|
|
337
|
+
}
|
|
320
338
|
} else {
|
|
321
339
|
resultProperty = property;
|
|
322
340
|
}
|
|
323
|
-
if (resultProperty.
|
|
341
|
+
if (resultProperty?.dynamicProps && rest.path) {
|
|
324
342
|
const path = rest.path;
|
|
325
|
-
if (!path) throw Error("Trying to resolve dynamicProps without specifying the entity path");
|
|
326
343
|
const usedPropertyValue = rest.propertyKey ? getIn$1(rest.values, rest.propertyKey) : void 0;
|
|
327
344
|
const dynamicPropsResult = resultProperty.dynamicProps({
|
|
328
345
|
...rest,
|
|
@@ -383,10 +400,14 @@ function resolveProperty(props) {
|
|
|
383
400
|
}
|
|
384
401
|
return resolvedProperty;
|
|
385
402
|
}
|
|
386
|
-
function resolveRelationProperty(property, relations) {
|
|
387
|
-
|
|
403
|
+
function resolveRelationProperty(property, relations, propertyKey) {
|
|
404
|
+
if (property.relation) {
|
|
405
|
+
return property;
|
|
406
|
+
}
|
|
407
|
+
const name = property.relationName || propertyKey;
|
|
408
|
+
const relation = name ? relations.find((rel) => rel.relationName === name) : void 0;
|
|
388
409
|
if (!relation) {
|
|
389
|
-
throw Error(`Relation ${
|
|
410
|
+
throw Error(`Relation ${name ?? "(unnamed)"} not found`);
|
|
390
411
|
}
|
|
391
412
|
return {
|
|
392
413
|
...property,
|
|
@@ -518,15 +539,35 @@ function getSubcollections(collection) {
|
|
|
518
539
|
if (collection.childCollections) {
|
|
519
540
|
return collection.childCollections() ?? [];
|
|
520
541
|
}
|
|
521
|
-
if (
|
|
542
|
+
if (getDataSourceCapabilities(collection.driver).supportsSubcollections && collection.subcollections) {
|
|
522
543
|
return collection.subcollections() ?? [];
|
|
523
544
|
}
|
|
524
|
-
if (
|
|
545
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations && collection.relations) {
|
|
525
546
|
const manyRelations = collection.relations.filter((r) => r.cardinality === "many");
|
|
526
547
|
return manyRelations.map((r) => {
|
|
527
548
|
const target = r.target();
|
|
528
|
-
|
|
529
|
-
|
|
549
|
+
if (!target) return void 0;
|
|
550
|
+
const relationKey = r.relationName || target.slug;
|
|
551
|
+
let customName;
|
|
552
|
+
if (collection.properties) {
|
|
553
|
+
const prop = Object.entries(collection.properties).find(([_, p]) => p.type === "relation" && p.relationName === relationKey);
|
|
554
|
+
if (prop && prop[1].name) {
|
|
555
|
+
customName = prop[1].name;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const baseOverrides = {
|
|
559
|
+
slug: relationKey
|
|
560
|
+
};
|
|
561
|
+
if (customName) {
|
|
562
|
+
baseOverrides.name = customName;
|
|
563
|
+
baseOverrides.singularName = customName;
|
|
564
|
+
}
|
|
565
|
+
const targetWithOverrides = {
|
|
566
|
+
...target,
|
|
567
|
+
...baseOverrides
|
|
568
|
+
};
|
|
569
|
+
return r.overrides ? mergeDeep(targetWithOverrides, r.overrides) : targetWithOverrides;
|
|
570
|
+
}).filter((c) => Boolean(c));
|
|
530
571
|
}
|
|
531
572
|
return [];
|
|
532
573
|
}
|
|
@@ -594,8 +635,8 @@ function evaluateAST(sqlString, auth, entity) {
|
|
|
594
635
|
const userRoles = auth.user?.roles || [];
|
|
595
636
|
return requiredRoles.every((r) => userRoles.includes(r));
|
|
596
637
|
}
|
|
597
|
-
const pattern1 = new RegExp(
|
|
598
|
-
const pattern2 = new RegExp(
|
|
638
|
+
const pattern1 = new RegExp("^\\{?([a-zA-Z0-9_]+)\\}?\\s*=\\s*(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))");
|
|
639
|
+
const pattern2 = new RegExp("^(?:current_setting\\s*\\(\\s*'app\\.user_id'\\s*\\)|auth\\.uid\\(\\))\\s*=\\s*\\{?([a-zA-Z0-9_]+)\\}?");
|
|
599
640
|
const match1 = cleanedSQL.match(pattern1);
|
|
600
641
|
if (match1 && match1[1]) {
|
|
601
642
|
return entity.values[match1[1]] === auth.user?.uid;
|
|
@@ -628,7 +669,7 @@ function evaluateRule(rule, auth, entity) {
|
|
|
628
669
|
return true;
|
|
629
670
|
}
|
|
630
671
|
function checkOperation(collection, authController, entity, targetOperation) {
|
|
631
|
-
const securityRules =
|
|
672
|
+
const securityRules = getDataSourceCapabilities(collection.driver).supportsRLS ? collection.securityRules : void 0;
|
|
632
673
|
if (!securityRules || securityRules.length === 0) {
|
|
633
674
|
return true;
|
|
634
675
|
}
|
|
@@ -1157,7 +1198,7 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1157
1198
|
if (!newRelation.foreignKeyOnTarget) {
|
|
1158
1199
|
let foundForeignKey = false;
|
|
1159
1200
|
try {
|
|
1160
|
-
const targetRelations = targetCollection.relations || [];
|
|
1201
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
|
|
1161
1202
|
for (const targetRel of targetRelations) {
|
|
1162
1203
|
if (targetRel.direction === "owning" && targetRel.cardinality === "one" && targetRel.localKey) {
|
|
1163
1204
|
try {
|
|
@@ -1183,7 +1224,7 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1183
1224
|
let isManyToManyInverse = false;
|
|
1184
1225
|
if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) {
|
|
1185
1226
|
try {
|
|
1186
|
-
const targetRelations = targetCollection.relations || [];
|
|
1227
|
+
const targetRelations = getDataSourceCapabilities(targetCollection.driver).supportsRelations ? targetCollection.relations || [] : [];
|
|
1187
1228
|
for (const targetRel of targetRelations) {
|
|
1188
1229
|
if (targetRel.cardinality === "many" && (targetRel.direction === "owning" || !targetRel.direction) && targetRel.relationName === newRelation.inverseRelationName) {
|
|
1189
1230
|
isManyToManyInverse = true;
|
|
@@ -1217,14 +1258,21 @@ function sanitizeRelation(relation, sourceCollection) {
|
|
|
1217
1258
|
}
|
|
1218
1259
|
return newRelation;
|
|
1219
1260
|
}
|
|
1261
|
+
const _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
|
|
1220
1262
|
function resolveCollectionRelations(collection) {
|
|
1263
|
+
const cached = _resolvedRelationsCache.get(collection);
|
|
1264
|
+
if (cached) return cached;
|
|
1265
|
+
if (!getDataSourceCapabilities(collection.driver).supportsRelations) return {};
|
|
1266
|
+
const relCollection = collection;
|
|
1221
1267
|
const relations = {};
|
|
1222
|
-
|
|
1223
|
-
|
|
1268
|
+
const registeredRelationNames = /* @__PURE__ */ new Set();
|
|
1269
|
+
if (relCollection.relations) {
|
|
1270
|
+
relCollection.relations.forEach((relation) => {
|
|
1224
1271
|
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1225
1272
|
const relationKey = normalizedRelation.relationName;
|
|
1226
1273
|
if (relationKey) {
|
|
1227
1274
|
relations[relationKey] = normalizedRelation;
|
|
1275
|
+
registeredRelationNames.add(relationKey);
|
|
1228
1276
|
}
|
|
1229
1277
|
});
|
|
1230
1278
|
}
|
|
@@ -1236,15 +1284,17 @@ function resolveCollectionRelations(collection) {
|
|
|
1236
1284
|
sourceCollection: collection
|
|
1237
1285
|
});
|
|
1238
1286
|
if (relation) {
|
|
1239
|
-
if (
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
relations[propKey] = sanitizeRelation(relation, collection);
|
|
1287
|
+
if (relations[propKey]) return;
|
|
1288
|
+
if (!relation.relationName) {
|
|
1289
|
+
relation.relationName = propKey;
|
|
1244
1290
|
}
|
|
1291
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
1292
|
+
relations[propKey] = normalizedRelation;
|
|
1293
|
+
registeredRelationNames.add(normalizedRelation.relationName ?? propKey);
|
|
1245
1294
|
}
|
|
1246
1295
|
});
|
|
1247
1296
|
}
|
|
1297
|
+
_resolvedRelationsCache.set(collection, relations);
|
|
1248
1298
|
return relations;
|
|
1249
1299
|
}
|
|
1250
1300
|
function resolvePropertyRelation({
|
|
@@ -1253,7 +1303,24 @@ function resolvePropertyRelation({
|
|
|
1253
1303
|
sourceCollection
|
|
1254
1304
|
}) {
|
|
1255
1305
|
if (property.type !== "relation") return void 0;
|
|
1256
|
-
const
|
|
1306
|
+
const relProp = property;
|
|
1307
|
+
if (relProp.target) {
|
|
1308
|
+
return {
|
|
1309
|
+
relationName: relProp.relationName || propertyKey,
|
|
1310
|
+
target: relProp.target,
|
|
1311
|
+
cardinality: relProp.cardinality || "one",
|
|
1312
|
+
direction: relProp.direction || "owning",
|
|
1313
|
+
inverseRelationName: relProp.inverseRelationName,
|
|
1314
|
+
localKey: relProp.localKey,
|
|
1315
|
+
foreignKeyOnTarget: relProp.foreignKeyOnTarget,
|
|
1316
|
+
through: relProp.through,
|
|
1317
|
+
joinPath: relProp.joinPath,
|
|
1318
|
+
onUpdate: relProp.onUpdate,
|
|
1319
|
+
onDelete: relProp.onDelete,
|
|
1320
|
+
overrides: relProp.overrides
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
const relation = (sourceCollection.relations ?? []).find((rel) => rel.relationName === relProp.relationName);
|
|
1257
1324
|
if (!relation) {
|
|
1258
1325
|
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug}'`);
|
|
1259
1326
|
return void 0;
|
|
@@ -1261,7 +1328,7 @@ function resolvePropertyRelation({
|
|
|
1261
1328
|
return relation;
|
|
1262
1329
|
}
|
|
1263
1330
|
function getTableName(collection) {
|
|
1264
|
-
if (
|
|
1331
|
+
if (getDataSourceCapabilities(collection.driver).supportsRelations) {
|
|
1265
1332
|
return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
1266
1333
|
}
|
|
1267
1334
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
@@ -1277,6 +1344,14 @@ function getEnumVarName(tableName, propName) {
|
|
|
1277
1344
|
function getColumnName(fullColumn) {
|
|
1278
1345
|
return fullColumn.includes(".") ? fullColumn.split(".").pop() : fullColumn;
|
|
1279
1346
|
}
|
|
1347
|
+
function findRelation(resolvedRelations, key) {
|
|
1348
|
+
if (resolvedRelations[key]) return resolvedRelations[key];
|
|
1349
|
+
const slugKey = key.replace(/_/g, "-");
|
|
1350
|
+
if (slugKey !== key && resolvedRelations[slugKey]) return resolvedRelations[slugKey];
|
|
1351
|
+
const snakeKey = key.replace(/-/g, "_");
|
|
1352
|
+
if (snakeKey !== key && resolvedRelations[snakeKey]) return resolvedRelations[snakeKey];
|
|
1353
|
+
return void 0;
|
|
1354
|
+
}
|
|
1280
1355
|
function getIn(obj, path) {
|
|
1281
1356
|
if (!obj || !path) return void 0;
|
|
1282
1357
|
return path.split(".").reduce((acc, part) => acc && acc[part], obj);
|
|
@@ -1372,7 +1447,7 @@ function applyPropertyConditions(property, context) {
|
|
|
1372
1447
|
conditions
|
|
1373
1448
|
} = property;
|
|
1374
1449
|
if (!conditions) return property;
|
|
1375
|
-
|
|
1450
|
+
const result = {
|
|
1376
1451
|
...property
|
|
1377
1452
|
};
|
|
1378
1453
|
if (conditions.disabled) {
|
|
@@ -1482,10 +1557,12 @@ class CollectionRegistry {
|
|
|
1482
1557
|
collectionsByTableName = /* @__PURE__ */ new Map();
|
|
1483
1558
|
collectionsBySlug = /* @__PURE__ */ new Map();
|
|
1484
1559
|
rootCollections = [];
|
|
1560
|
+
cachedCollectionsList = null;
|
|
1485
1561
|
// Raw configuration layer (used by Collection Editor AST generator)
|
|
1486
1562
|
rawCollectionsByTableName = /* @__PURE__ */ new Map();
|
|
1487
1563
|
rawCollectionsBySlug = /* @__PURE__ */ new Map();
|
|
1488
1564
|
rawRootCollections = [];
|
|
1565
|
+
cachedRawCollectionsList = null;
|
|
1489
1566
|
// Snapshot of raw input for idempotency check — compared BEFORE normalization
|
|
1490
1567
|
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
1491
1568
|
lastRawInputSnapshot = null;
|
|
@@ -1498,9 +1575,11 @@ class CollectionRegistry {
|
|
|
1498
1575
|
this.collectionsByTableName.clear();
|
|
1499
1576
|
this.collectionsBySlug.clear();
|
|
1500
1577
|
this.rootCollections = [];
|
|
1578
|
+
this.cachedCollectionsList = null;
|
|
1501
1579
|
this.rawCollectionsByTableName.clear();
|
|
1502
1580
|
this.rawCollectionsBySlug.clear();
|
|
1503
1581
|
this.rawRootCollections = [];
|
|
1582
|
+
this.cachedRawCollectionsList = null;
|
|
1504
1583
|
}
|
|
1505
1584
|
/**
|
|
1506
1585
|
* Registers a collection and its subcollections recursively.
|
|
@@ -1533,12 +1612,14 @@ class CollectionRegistry {
|
|
|
1533
1612
|
this.rawCollectionsBySlug.set(raw.slug, raw);
|
|
1534
1613
|
}
|
|
1535
1614
|
});
|
|
1536
|
-
normalizedCollections.forEach((c
|
|
1615
|
+
normalizedCollections.forEach((c) => {
|
|
1537
1616
|
const subcollections = getSubcollections(c);
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
this._registerRecursively(this.normalizeCollection(
|
|
1617
|
+
if (subcollections && subcollections.length > 0) {
|
|
1618
|
+
subcollections.forEach((subCollection) => {
|
|
1619
|
+
if (!subCollection) return;
|
|
1620
|
+
this._registerRecursively(this.normalizeCollection({
|
|
1621
|
+
...subCollection
|
|
1622
|
+
}), cloneDeep(subCollection));
|
|
1542
1623
|
});
|
|
1543
1624
|
}
|
|
1544
1625
|
});
|
|
@@ -1564,41 +1645,100 @@ class CollectionRegistry {
|
|
|
1564
1645
|
if (rawCollection.slug) {
|
|
1565
1646
|
this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
|
|
1566
1647
|
}
|
|
1567
|
-
const subcollections = getSubcollections(
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
this._registerRecursively(this.normalizeCollection(
|
|
1648
|
+
const subcollections = getSubcollections(normalizedCollection);
|
|
1649
|
+
if (subcollections && subcollections.length > 0) {
|
|
1650
|
+
subcollections.forEach((subCollection) => {
|
|
1651
|
+
if (!subCollection) return;
|
|
1652
|
+
this._registerRecursively(this.normalizeCollection({
|
|
1653
|
+
...subCollection
|
|
1654
|
+
}), cloneDeep(subCollection));
|
|
1572
1655
|
});
|
|
1573
1656
|
}
|
|
1574
1657
|
}
|
|
1575
1658
|
normalizeCollection(collection) {
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1659
|
+
const result = {
|
|
1660
|
+
...collection
|
|
1661
|
+
};
|
|
1662
|
+
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
1663
|
+
const relResult = result;
|
|
1664
|
+
const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
|
|
1665
|
+
const mergedRelationsRaw = [...extractedRelations];
|
|
1666
|
+
for (const manual of manualRelations) {
|
|
1667
|
+
const name = manual.relationName;
|
|
1668
|
+
if (!name || !mergedRelationsRaw.find((r) => r.relationName === name)) {
|
|
1669
|
+
mergedRelationsRaw.push(manual);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
let mergedRelations = mergedRelationsRaw;
|
|
1673
|
+
if (getDataSourceCapabilities(result.driver).supportsRelations) {
|
|
1674
|
+
mergedRelations = mergedRelationsRaw.map((r) => {
|
|
1675
|
+
try {
|
|
1676
|
+
return sanitizeRelation(r, result);
|
|
1677
|
+
} catch {
|
|
1678
|
+
return r;
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
relResult.relations = mergedRelations;
|
|
1682
|
+
}
|
|
1683
|
+
const properties = this.normalizeProperties(result.properties, mergedRelations);
|
|
1684
|
+
result.properties = properties;
|
|
1685
|
+
if (!result.childCollections) {
|
|
1686
|
+
if (getDataSourceCapabilities(result.driver).supportsSubcollections && result.subcollections) {
|
|
1687
|
+
result.childCollections = result.subcollections;
|
|
1688
|
+
} else if (getDataSourceCapabilities(result.driver).supportsRelations && relResult.relations) {
|
|
1689
|
+
const manyRelations = relResult.relations.filter((r) => r.cardinality === "many");
|
|
1584
1690
|
if (manyRelations.length > 0) {
|
|
1585
|
-
|
|
1691
|
+
result.childCollections = () => manyRelations.map((r) => {
|
|
1586
1692
|
const target = r.target();
|
|
1587
1693
|
return r.overrides ? mergeDeep(target, r.overrides) : target;
|
|
1588
1694
|
});
|
|
1589
1695
|
}
|
|
1590
1696
|
}
|
|
1591
1697
|
}
|
|
1592
|
-
return
|
|
1698
|
+
return result;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Extract Relation[] from properties that have inline relation config (i.e. `target` is set).
|
|
1702
|
+
* This allows developers to define relations directly on properties without a separate
|
|
1703
|
+
* `relations[]` entry on the collection.
|
|
1704
|
+
*/
|
|
1705
|
+
extractRelationsFromProperties(properties) {
|
|
1706
|
+
const relations = [];
|
|
1707
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
1708
|
+
if (property.type === "relation") {
|
|
1709
|
+
const relProp = property;
|
|
1710
|
+
const target = relProp.target ?? relProp.relation?.target;
|
|
1711
|
+
if (target) {
|
|
1712
|
+
const relationName = relProp.relationName ?? relProp.relation?.relationName ?? key;
|
|
1713
|
+
relations.push({
|
|
1714
|
+
relationName,
|
|
1715
|
+
target,
|
|
1716
|
+
cardinality: relProp.cardinality ?? relProp.relation?.cardinality ?? "one",
|
|
1717
|
+
direction: relProp.direction ?? relProp.relation?.direction ?? "owning",
|
|
1718
|
+
inverseRelationName: relProp.inverseRelationName ?? relProp.relation?.inverseRelationName,
|
|
1719
|
+
localKey: relProp.localKey ?? relProp.relation?.localKey,
|
|
1720
|
+
foreignKeyOnTarget: relProp.foreignKeyOnTarget ?? relProp.relation?.foreignKeyOnTarget,
|
|
1721
|
+
through: relProp.through ?? relProp.relation?.through,
|
|
1722
|
+
joinPath: relProp.joinPath ?? relProp.relation?.joinPath,
|
|
1723
|
+
onUpdate: relProp.onUpdate ?? relProp.relation?.onUpdate,
|
|
1724
|
+
onDelete: relProp.onDelete ?? relProp.relation?.onDelete,
|
|
1725
|
+
overrides: relProp.overrides ?? relProp.relation?.overrides
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
} else if (property.type === "map" && property.properties) {
|
|
1729
|
+
relations.push(...this.extractRelationsFromProperties(property.properties));
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return relations;
|
|
1593
1733
|
}
|
|
1594
1734
|
normalizeProperties(properties, relations) {
|
|
1595
1735
|
const newProperties = {};
|
|
1596
1736
|
for (const key in properties) {
|
|
1597
|
-
newProperties[key] = this.normalizeProperty(properties[key], relations);
|
|
1737
|
+
newProperties[key] = this.normalizeProperty(key, properties[key], relations);
|
|
1598
1738
|
}
|
|
1599
1739
|
return newProperties;
|
|
1600
1740
|
}
|
|
1601
|
-
normalizeProperty(property, relations) {
|
|
1741
|
+
normalizeProperty(key, property, relations) {
|
|
1602
1742
|
const newProperty = {
|
|
1603
1743
|
...property
|
|
1604
1744
|
};
|
|
@@ -1608,9 +1748,9 @@ class CollectionRegistry {
|
|
|
1608
1748
|
const arrayProp = newProperty;
|
|
1609
1749
|
if (arrayProp.of) {
|
|
1610
1750
|
if (Array.isArray(arrayProp.of)) {
|
|
1611
|
-
arrayProp.of = arrayProp.of.map((p) => this.normalizeProperty(p, relations));
|
|
1751
|
+
arrayProp.of = arrayProp.of.map((p, i) => this.normalizeProperty(`${key}[${i}]`, p, relations));
|
|
1612
1752
|
} else {
|
|
1613
|
-
arrayProp.of = this.normalizeProperty(arrayProp.of, relations);
|
|
1753
|
+
arrayProp.of = this.normalizeProperty(`${key}.of`, arrayProp.of, relations);
|
|
1614
1754
|
}
|
|
1615
1755
|
} else if (arrayProp.oneOf && arrayProp.oneOf.properties) {
|
|
1616
1756
|
arrayProp.oneOf.properties = this.normalizeProperties(arrayProp.oneOf.properties, relations);
|
|
@@ -1622,11 +1762,12 @@ class CollectionRegistry {
|
|
|
1622
1762
|
}
|
|
1623
1763
|
} else if (newProperty.type === "relation") {
|
|
1624
1764
|
const relationProperty = newProperty;
|
|
1625
|
-
const
|
|
1765
|
+
const name = relationProperty.relationName || key;
|
|
1766
|
+
const relation = relations.find((r) => r.relationName === name);
|
|
1626
1767
|
if (relation) {
|
|
1627
1768
|
relationProperty.relation = relation;
|
|
1628
1769
|
} else {
|
|
1629
|
-
console.warn(`Could not find relation for property with relationName: ${
|
|
1770
|
+
console.warn(`Could not find relation for property '${key}' with relationName: ${name}`);
|
|
1630
1771
|
}
|
|
1631
1772
|
}
|
|
1632
1773
|
return newProperty;
|
|
@@ -1634,6 +1775,11 @@ class CollectionRegistry {
|
|
|
1634
1775
|
get(path) {
|
|
1635
1776
|
const bySlug = this.collectionsBySlug.get(path);
|
|
1636
1777
|
if (bySlug) return bySlug;
|
|
1778
|
+
if (path.includes("-")) {
|
|
1779
|
+
const normalized = path.replace(/-/g, "_");
|
|
1780
|
+
const byNormalized = this.collectionsBySlug.get(normalized);
|
|
1781
|
+
if (byNormalized) return byNormalized;
|
|
1782
|
+
}
|
|
1637
1783
|
return this.collectionsByTableName.get(path);
|
|
1638
1784
|
}
|
|
1639
1785
|
/**
|
|
@@ -1643,6 +1789,11 @@ class CollectionRegistry {
|
|
|
1643
1789
|
getRaw(path) {
|
|
1644
1790
|
const bySlug = this.rawCollectionsBySlug.get(path);
|
|
1645
1791
|
if (bySlug) return bySlug;
|
|
1792
|
+
if (path.includes("-")) {
|
|
1793
|
+
const normalized = path.replace(/-/g, "_");
|
|
1794
|
+
const byNormalized = this.rawCollectionsBySlug.get(normalized);
|
|
1795
|
+
if (byNormalized) return byNormalized;
|
|
1796
|
+
}
|
|
1646
1797
|
return this.rawCollectionsByTableName.get(path);
|
|
1647
1798
|
}
|
|
1648
1799
|
/**
|
|
@@ -1664,11 +1815,11 @@ class CollectionRegistry {
|
|
|
1664
1815
|
}
|
|
1665
1816
|
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
1666
1817
|
const relationKey = pathSegments[i];
|
|
1667
|
-
if (!
|
|
1668
|
-
throw new Error(`Relation path navigation requires a
|
|
1818
|
+
if (!getDataSourceCapabilities(currentCollection.driver).supportsRelations) {
|
|
1819
|
+
throw new Error(`Relation path navigation requires a collection that supports relations, but '${currentCollection.slug}' uses driver '${currentCollection.driver}'`);
|
|
1669
1820
|
}
|
|
1670
1821
|
const resolvedRelations = resolveCollectionRelations(currentCollection);
|
|
1671
|
-
const relation = resolvedRelations
|
|
1822
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
1672
1823
|
if (!relation) {
|
|
1673
1824
|
throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
1674
1825
|
}
|
|
@@ -1678,10 +1829,16 @@ class CollectionRegistry {
|
|
|
1678
1829
|
return currentCollection;
|
|
1679
1830
|
}
|
|
1680
1831
|
getCollections() {
|
|
1681
|
-
|
|
1832
|
+
if (!this.cachedCollectionsList) {
|
|
1833
|
+
this.cachedCollectionsList = Array.from(this.collectionsByTableName.values());
|
|
1834
|
+
}
|
|
1835
|
+
return this.cachedCollectionsList;
|
|
1682
1836
|
}
|
|
1683
1837
|
getRawCollections() {
|
|
1684
|
-
|
|
1838
|
+
if (!this.cachedRawCollectionsList) {
|
|
1839
|
+
this.cachedRawCollectionsList = Array.from(this.rawCollectionsByTableName.values());
|
|
1840
|
+
}
|
|
1841
|
+
return this.cachedRawCollectionsList;
|
|
1685
1842
|
}
|
|
1686
1843
|
/**
|
|
1687
1844
|
* Resolves a multi-segment path like "products/123/locales" and returns
|
|
@@ -1737,24 +1894,62 @@ function convertWhereToFilter(where) {
|
|
|
1737
1894
|
"lte": "<=",
|
|
1738
1895
|
"in": "in",
|
|
1739
1896
|
"nin": "not-in",
|
|
1897
|
+
"not-in": "not-in",
|
|
1740
1898
|
"cs": "array-contains",
|
|
1741
|
-
"csa": "array-contains-any"
|
|
1899
|
+
"csa": "array-contains-any",
|
|
1900
|
+
"==": "==",
|
|
1901
|
+
"!=": "!=",
|
|
1902
|
+
">": ">",
|
|
1903
|
+
">=": ">=",
|
|
1904
|
+
"<": "<",
|
|
1905
|
+
"<=": "<=",
|
|
1906
|
+
"array-contains": "array-contains",
|
|
1907
|
+
"array-contains-any": "array-contains-any"
|
|
1742
1908
|
};
|
|
1743
1909
|
const filter = {};
|
|
1744
1910
|
for (const [field, rawValue] of Object.entries(where)) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
if (typeof
|
|
1750
|
-
|
|
1911
|
+
if (rawValue === null) {
|
|
1912
|
+
filter[field] = ["==", null];
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
if (typeof rawValue === "boolean") {
|
|
1916
|
+
filter[field] = ["==", rawValue];
|
|
1917
|
+
continue;
|
|
1751
1918
|
}
|
|
1752
|
-
if (typeof
|
|
1753
|
-
|
|
1919
|
+
if (typeof rawValue === "number") {
|
|
1920
|
+
filter[field] = ["==", rawValue];
|
|
1921
|
+
continue;
|
|
1754
1922
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1923
|
+
if (Array.isArray(rawValue) && rawValue.length === 2) {
|
|
1924
|
+
const [rawOp, val] = rawValue;
|
|
1925
|
+
const mappedOp = operatorMap[rawOp] ?? "==";
|
|
1926
|
+
filter[field] = [mappedOp, val];
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
if (typeof rawValue === "string") {
|
|
1930
|
+
const dotIndex = rawValue.indexOf(".");
|
|
1931
|
+
if (dotIndex === -1) {
|
|
1932
|
+
filter[field] = ["==", rawValue];
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const op = rawValue.substring(0, dotIndex);
|
|
1936
|
+
let value = rawValue.substring(dotIndex + 1);
|
|
1937
|
+
if (typeof value === "string" && value.startsWith("(") && value.endsWith(")")) {
|
|
1938
|
+
value = value.slice(1, -1).split(",").map((v) => v.trim());
|
|
1939
|
+
}
|
|
1940
|
+
if (value === "null") {
|
|
1941
|
+
value = null;
|
|
1942
|
+
} else if (value === "true") {
|
|
1943
|
+
value = true;
|
|
1944
|
+
} else if (value === "false") {
|
|
1945
|
+
value = false;
|
|
1946
|
+
} else if (typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "") {
|
|
1947
|
+
value = Number(value);
|
|
1948
|
+
}
|
|
1949
|
+
const mappedOp = operatorMap[op];
|
|
1950
|
+
if (mappedOp) {
|
|
1951
|
+
filter[field] = [mappedOp, value];
|
|
1952
|
+
}
|
|
1758
1953
|
}
|
|
1759
1954
|
}
|
|
1760
1955
|
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
@@ -1773,7 +1968,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
1773
1968
|
const entities = await driver.fetchCollection({
|
|
1774
1969
|
path: slug,
|
|
1775
1970
|
limit: params?.limit,
|
|
1776
|
-
|
|
1971
|
+
offset: params?.offset,
|
|
1777
1972
|
filter: convertWhereToFilter(params?.where),
|
|
1778
1973
|
orderBy: orderParsed?.[0],
|
|
1779
1974
|
order: orderParsed?.[1],
|
|
@@ -1835,7 +2030,7 @@ function createDriverAccessor(driver, slug) {
|
|
|
1835
2030
|
return driver.listenCollection({
|
|
1836
2031
|
path: slug,
|
|
1837
2032
|
limit: params?.limit,
|
|
1838
|
-
|
|
2033
|
+
offset: params?.offset,
|
|
1839
2034
|
filter: convertWhereToFilter(params?.where),
|
|
1840
2035
|
orderBy: orderParsed?.[0],
|
|
1841
2036
|
order: orderParsed?.[1],
|
|
@@ -1882,7 +2077,8 @@ function buildRebaseData(driver) {
|
|
|
1882
2077
|
if (prop === "collection") return getAccessor;
|
|
1883
2078
|
if (typeof prop === "symbol") return void 0;
|
|
1884
2079
|
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
|
|
1885
|
-
|
|
2080
|
+
const slug = toSnakeCase(prop);
|
|
2081
|
+
return getAccessor(slug);
|
|
1886
2082
|
}
|
|
1887
2083
|
});
|
|
1888
2084
|
}
|
|
@@ -1908,8 +2104,11 @@ export {
|
|
|
1908
2104
|
canDeleteEntity,
|
|
1909
2105
|
canEditEntity,
|
|
1910
2106
|
canReadCollection,
|
|
2107
|
+
createRelationRef,
|
|
2108
|
+
createRelationRefWithData,
|
|
1911
2109
|
enumToObjectEntries,
|
|
1912
2110
|
evaluateCondition,
|
|
2111
|
+
findRelation,
|
|
1913
2112
|
fullPathToCollectionSegments,
|
|
1914
2113
|
getArrayResolvedProperties,
|
|
1915
2114
|
getCollectionBySlugWithin,
|