@rebasepro/common 0.0.1-canary.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/collections/CollectionRegistry.d.ts +48 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +2380 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2379 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/util/arrays.d.ts +1 -0
- package/dist/util/builders.d.ts +64 -0
- package/dist/util/callbacks.d.ts +6 -0
- package/dist/util/collections.d.ts +11 -0
- package/dist/util/common.d.ts +2 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/dates.d.ts +1 -0
- package/dist/util/entities.d.ts +28 -0
- package/dist/util/entity_actions.d.ts +2 -0
- package/dist/util/enums.d.ts +3 -0
- package/dist/util/fields.d.ts +2 -0
- package/dist/util/flatten_object.d.ts +5 -0
- package/dist/util/hash.d.ts +1 -0
- package/dist/util/index.d.ts +26 -0
- package/dist/util/names.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +29 -0
- package/dist/util/navigation_utils.d.ts +31 -0
- package/dist/util/objects.d.ts +26 -0
- package/dist/util/os.d.ts +2 -0
- package/dist/util/parent_references_from_path.d.ts +6 -0
- package/dist/util/paths.d.ts +14 -0
- package/dist/util/permissions.d.ts +5 -0
- package/dist/util/permissions.test.d.ts +1 -0
- package/dist/util/plurals.d.ts +16 -0
- package/dist/util/references.d.ts +2 -0
- package/dist/util/regexp.d.ts +7 -0
- package/dist/util/relations.d.ts +12 -0
- package/dist/util/resolutions.d.ts +74 -0
- package/dist/util/storage.d.ts +24 -0
- package/dist/util/strings.d.ts +7 -0
- package/package.json +118 -0
- package/src/collections/CollectionRegistry.ts +319 -0
- package/src/collections/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/util/arrays.ts +3 -0
- package/src/util/builders.ts +138 -0
- package/src/util/callbacks.ts +115 -0
- package/src/util/collections.ts +126 -0
- package/src/util/common.ts +2 -0
- package/src/util/conditions.ts +348 -0
- package/src/util/dates.ts +1 -0
- package/src/util/entities.ts +212 -0
- package/src/util/entity_actions.ts +28 -0
- package/src/util/enums.ts +26 -0
- package/src/util/fields.ts +28 -0
- package/src/util/flatten_object.ts +45 -0
- package/src/util/hash.ts +11 -0
- package/src/util/index.ts +26 -0
- package/src/util/names.ts +30 -0
- package/src/util/navigation_from_path.ts +121 -0
- package/src/util/navigation_utils.ts +222 -0
- package/src/util/objects.ts +376 -0
- package/src/util/os.ts +13 -0
- package/src/util/parent_references_from_path.ts +57 -0
- package/src/util/paths.ts +27 -0
- package/src/util/permissions.test.ts +716 -0
- package/src/util/permissions.ts +235 -0
- package/src/util/plurals.ts +188 -0
- package/src/util/references.ts +34 -0
- package/src/util/regexp.ts +32 -0
- package/src/util/relations.ts +211 -0
- package/src/util/resolutions.ts +383 -0
- package/src/util/storage.ts +144 -0
- package/src/util/strings.ts +84 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { EntityCollection, Property, Relation } from "@rebasepro/types";
|
|
2
|
+
import { toSnakeCase } from "./strings";
|
|
3
|
+
import { generateForeignKeyName } from "./names";
|
|
4
|
+
|
|
5
|
+
export function sanitizeRelation(relation: Partial<Relation>, sourceCollection: EntityCollection): Relation {
|
|
6
|
+
if (!relation.target) {
|
|
7
|
+
throw new Error("Relation is missing a `target` collection.");
|
|
8
|
+
}
|
|
9
|
+
const targetCollection = relation.target();
|
|
10
|
+
|
|
11
|
+
const newRelation: Partial<Relation> = { ...relation };
|
|
12
|
+
|
|
13
|
+
// 1. Default relationName from target collection slug or dbPath
|
|
14
|
+
if (!newRelation.relationName) {
|
|
15
|
+
newRelation.relationName = toSnakeCase(targetCollection.slug ?? targetCollection.dbPath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Infer or default direction if absent
|
|
19
|
+
if (!newRelation.direction) {
|
|
20
|
+
if (newRelation.foreignKeyOnTarget) newRelation.direction = "inverse";
|
|
21
|
+
else if (newRelation.through) newRelation.direction = "owning";
|
|
22
|
+
else if (newRelation.cardinality === "many") newRelation.direction = "inverse"; // Default has-many to be inverse
|
|
23
|
+
else newRelation.direction = "owning"; // Default all others to owning
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Do not default keys if a custom joinPath is provided; it's an advanced override.
|
|
27
|
+
if (!newRelation.joinPath) {
|
|
28
|
+
const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);
|
|
29
|
+
|
|
30
|
+
// 3. Default keys based on the relation type (cardinality and direction)
|
|
31
|
+
if (newRelation.cardinality === "one" && newRelation.direction === "owning") {
|
|
32
|
+
// Belongs-to / many-to-one
|
|
33
|
+
if (!newRelation.localKey) {
|
|
34
|
+
newRelation.localKey = generateForeignKeyName(newRelation.relationName);
|
|
35
|
+
}
|
|
36
|
+
} else if (newRelation.cardinality === "one" && newRelation.direction === "inverse") {
|
|
37
|
+
// Inverse one-to-one: the foreign key is on the target table pointing back to this collection
|
|
38
|
+
if (!newRelation.foreignKeyOnTarget) {
|
|
39
|
+
// First, try to find the corresponding owning relation's localKey on the target collection
|
|
40
|
+
let foundForeignKey = false;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Look for an owning relation on the target that points back to this collection
|
|
44
|
+
const targetRelations = targetCollection.relations || [];
|
|
45
|
+
for (const targetRel of targetRelations) {
|
|
46
|
+
if (targetRel.direction === "owning" &&
|
|
47
|
+
targetRel.cardinality === "one" &&
|
|
48
|
+
targetRel.localKey) {
|
|
49
|
+
try {
|
|
50
|
+
const targetRelTarget = targetRel.target();
|
|
51
|
+
if (targetRelTarget.slug === sourceCollection.slug ||
|
|
52
|
+
targetRelTarget.dbPath === sourceCollection.dbPath) {
|
|
53
|
+
// Found the corresponding owning relation, use its localKey
|
|
54
|
+
newRelation.foreignKeyOnTarget = targetRel.localKey;
|
|
55
|
+
foundForeignKey = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Continue looking if we can't resolve this target
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// If we can't inspect the target collection, fall back to naming convention
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If we couldn't find an explicit foreign key, fall back to naming convention
|
|
69
|
+
if (!foundForeignKey) {
|
|
70
|
+
const keyPrefix = newRelation.inverseRelationName
|
|
71
|
+
? toSnakeCase(newRelation.inverseRelationName)
|
|
72
|
+
: sourceName;
|
|
73
|
+
newRelation.foreignKeyOnTarget = generateForeignKeyName(keyPrefix);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else if (newRelation.cardinality === "many" && newRelation.direction === "inverse") {
|
|
77
|
+
// This could be either one-to-many or many-to-many inverse relation
|
|
78
|
+
// We need to check if there's a corresponding owning many-to-many relation
|
|
79
|
+
|
|
80
|
+
let isManyToManyInverse = false;
|
|
81
|
+
|
|
82
|
+
// Try to determine if this is a many-to-many inverse relation
|
|
83
|
+
if (newRelation.inverseRelationName && !newRelation.foreignKeyOnTarget) {
|
|
84
|
+
try {
|
|
85
|
+
// Look for a corresponding owning many-to-many relation on the target collection
|
|
86
|
+
const targetRelations = targetCollection.relations || [];
|
|
87
|
+
for (const targetRel of targetRelations) {
|
|
88
|
+
if (targetRel.cardinality === "many" &&
|
|
89
|
+
targetRel.direction === "owning" &&
|
|
90
|
+
targetRel.through &&
|
|
91
|
+
(targetRel.relationName === newRelation.inverseRelationName)) {
|
|
92
|
+
// Found a corresponding owning many-to-many relation
|
|
93
|
+
isManyToManyInverse = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// If we can't inspect the target collection, assume one-to-many
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Only add foreignKeyOnTarget for one-to-many inverse relations
|
|
103
|
+
if (!isManyToManyInverse && !newRelation.foreignKeyOnTarget) {
|
|
104
|
+
newRelation.foreignKeyOnTarget = generateForeignKeyName(sourceName);
|
|
105
|
+
}
|
|
106
|
+
} else if (newRelation.cardinality === "many" && newRelation.direction === "owning") {
|
|
107
|
+
|
|
108
|
+
// Many-to-many via junction table
|
|
109
|
+
const sourceTableName = getTableName(sourceCollection);
|
|
110
|
+
const targetTableName = getTableName(targetCollection);
|
|
111
|
+
|
|
112
|
+
newRelation.through = {
|
|
113
|
+
table: newRelation.through?.table ?? [sourceTableName, targetTableName].sort().join("_"),
|
|
114
|
+
sourceColumn: newRelation.through?.sourceColumn ?? generateForeignKeyName(sourceName),
|
|
115
|
+
targetColumn: newRelation.through?.targetColumn ?? generateForeignKeyName(newRelation.relationName),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. Basic validation to catch configuration errors early
|
|
121
|
+
if (newRelation.cardinality === "one" && newRelation.direction === "owning" && !newRelation.localKey && !newRelation.joinPath) {
|
|
122
|
+
throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'owning' one-to-one relation requires a 'localKey'. Check the relation config for '${newRelation.relationName}'`);
|
|
123
|
+
}
|
|
124
|
+
if (newRelation.cardinality === "one" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath) {
|
|
125
|
+
throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-one relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
|
|
126
|
+
}
|
|
127
|
+
if (newRelation.cardinality === "many" && newRelation.direction === "inverse" && !newRelation.foreignKeyOnTarget && !newRelation.joinPath) {
|
|
128
|
+
throw new Error(`Configuration Error in relation from '${sourceCollection.name}': An 'inverse' one-to-many relation requires a 'foreignKeyOnTarget'. Check the relation config for '${newRelation.relationName}'`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return newRelation as Relation;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function resolveCollectionRelations(
|
|
135
|
+
collection: EntityCollection,
|
|
136
|
+
): Record<string, Relation> {
|
|
137
|
+
const relations: Record<string, Relation> = {};
|
|
138
|
+
|
|
139
|
+
// 1. Process explicit relations from the new `relations` field
|
|
140
|
+
if (collection.relations) {
|
|
141
|
+
collection.relations.forEach((relation) => {
|
|
142
|
+
const normalizedRelation = sanitizeRelation(relation, collection);
|
|
143
|
+
const relationKey = normalizedRelation.relationName;
|
|
144
|
+
if (relationKey) {
|
|
145
|
+
relations[relationKey] = normalizedRelation;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Process properties of type "relation"
|
|
151
|
+
if (collection.properties) {
|
|
152
|
+
Object.entries(collection.properties).forEach(([propKey, prop]) => {
|
|
153
|
+
const relation = resolvePropertyRelation({
|
|
154
|
+
propertyKey: propKey,
|
|
155
|
+
property: prop as Property,
|
|
156
|
+
sourceCollection: collection
|
|
157
|
+
});
|
|
158
|
+
if (relation) {
|
|
159
|
+
// Use property name as relation key if not already defined
|
|
160
|
+
if (!relations[propKey]) {
|
|
161
|
+
// FIX: Set relationName to propKey if not defined, before normalizing
|
|
162
|
+
if (!relation.relationName) {
|
|
163
|
+
relation.relationName = propKey;
|
|
164
|
+
}
|
|
165
|
+
relations[propKey] = sanitizeRelation(relation, collection); // Already normalized in collection.relations
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return relations;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function resolvePropertyRelation({
|
|
175
|
+
propertyKey,
|
|
176
|
+
property,
|
|
177
|
+
sourceCollection
|
|
178
|
+
}: {
|
|
179
|
+
propertyKey: string;
|
|
180
|
+
property: Property;
|
|
181
|
+
sourceCollection: EntityCollection;
|
|
182
|
+
}): Relation | undefined {
|
|
183
|
+
if (property.type !== "relation") return undefined;
|
|
184
|
+
|
|
185
|
+
const relation = sourceCollection.relations?.find((rel) => rel.relationName === property.relationName)
|
|
186
|
+
if (!relation) {
|
|
187
|
+
console.warn(`Unrecognized relation format for property '${propertyKey}' in collection '${sourceCollection.slug || sourceCollection.dbPath}'`);
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return relation as Relation;
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getTableName(collection: EntityCollection): string {
|
|
196
|
+
return collection.dbPath ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getTableVarName(tableName: string): string {
|
|
200
|
+
return tableName.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getEnumVarName(tableName: string, propName: string): string {
|
|
204
|
+
const tableVar = getTableVarName(tableName);
|
|
205
|
+
const propVar = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
206
|
+
return `${tableVar}${propVar}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getColumnName(fullColumn: string): string {
|
|
210
|
+
return fullColumn.includes(".") ? fullColumn.split(".").pop()! : fullColumn;
|
|
211
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayProperty,
|
|
3
|
+
AuthController,
|
|
4
|
+
CustomizationController,
|
|
5
|
+
EntityAction,
|
|
6
|
+
EntityCollection,
|
|
7
|
+
EntityCustomView,
|
|
8
|
+
EnumValueConfig,
|
|
9
|
+
EnumValues,
|
|
10
|
+
NumberProperty,
|
|
11
|
+
Properties,
|
|
12
|
+
Property,
|
|
13
|
+
PropertyConfig,
|
|
14
|
+
Relation,
|
|
15
|
+
RelationProperty,
|
|
16
|
+
StringProperty
|
|
17
|
+
} from "@rebasepro/types";
|
|
18
|
+
import { isDefaultFieldConfigId } from "./fields";
|
|
19
|
+
import { isPropertyBuilder } from "./entities";
|
|
20
|
+
import { getIn, mergeDeep } from "./objects";
|
|
21
|
+
import { enumToObjectEntries } from "./enums";
|
|
22
|
+
import { DEFAULT_ONE_OF_TYPE } from "./common";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve property builders, enums and arrays.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type ResolvePropertyProps<M extends Record<string, any> = any> = {
|
|
29
|
+
property: Property
|
|
30
|
+
propertyKey?: string,
|
|
31
|
+
values?: Partial<M>,
|
|
32
|
+
previousValues?: Partial<M>,
|
|
33
|
+
path?: string,
|
|
34
|
+
entityId?: string | number,
|
|
35
|
+
index?: number,
|
|
36
|
+
propertyConfigs?: Record<string, PropertyConfig>;
|
|
37
|
+
ignoreMissingFields?: boolean;
|
|
38
|
+
authController: AuthController;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveProperty<M extends Record<string, any> = any>(props: ResolvePropertyProps<M>): Property | null {
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
property,
|
|
45
|
+
ignoreMissingFields = false,
|
|
46
|
+
...rest
|
|
47
|
+
} = props;
|
|
48
|
+
|
|
49
|
+
let resultProperty: Property;
|
|
50
|
+
|
|
51
|
+
if (isPropertyBuilder(property)) {
|
|
52
|
+
const path = rest.path;
|
|
53
|
+
if (!path)
|
|
54
|
+
throw Error("Trying to resolve a property builder without specifying the entity path");
|
|
55
|
+
|
|
56
|
+
const usedPropertyValue = rest.propertyKey ? getIn(rest.values, rest.propertyKey) : undefined;
|
|
57
|
+
const dynamicProps = property.dynamicProps?.({
|
|
58
|
+
...rest,
|
|
59
|
+
path,
|
|
60
|
+
propertyValue: usedPropertyValue,
|
|
61
|
+
values: rest.values ?? {},
|
|
62
|
+
previousValues: rest.previousValues ?? rest.values ?? {}
|
|
63
|
+
});
|
|
64
|
+
resultProperty = mergeDeep(property, dynamicProps ?? {});
|
|
65
|
+
} else {
|
|
66
|
+
resultProperty = property as Property;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Apply dynamic properties if they exist
|
|
70
|
+
if (resultProperty.dynamicProps) {
|
|
71
|
+
const path = rest.path;
|
|
72
|
+
if (!path)
|
|
73
|
+
throw Error("Trying to resolve dynamicProps without specifying the entity path");
|
|
74
|
+
|
|
75
|
+
const usedPropertyValue = rest.propertyKey ? getIn(rest.values, rest.propertyKey) : undefined;
|
|
76
|
+
const dynamicPropsResult = resultProperty.dynamicProps({
|
|
77
|
+
...rest,
|
|
78
|
+
path,
|
|
79
|
+
propertyValue: usedPropertyValue,
|
|
80
|
+
values: rest.values ?? {},
|
|
81
|
+
previousValues: rest.previousValues ?? rest.values ?? {}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (dynamicPropsResult) {
|
|
85
|
+
resultProperty = mergeDeep(resultProperty, dynamicPropsResult);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let resolvedProperty: Property | null;
|
|
90
|
+
|
|
91
|
+
if (resultProperty?.type === "map" && resultProperty.properties) {
|
|
92
|
+
const properties = resolveProperties({
|
|
93
|
+
ignoreMissingFields,
|
|
94
|
+
...rest,
|
|
95
|
+
properties: resultProperty.properties,
|
|
96
|
+
});
|
|
97
|
+
resolvedProperty = {
|
|
98
|
+
...resultProperty,
|
|
99
|
+
properties
|
|
100
|
+
} as Property;
|
|
101
|
+
} else if (resultProperty?.type === "array") {
|
|
102
|
+
resolvedProperty = resultProperty;
|
|
103
|
+
} else if ((resultProperty?.type === "string" || resultProperty?.type === "number") && resultProperty.enum) {
|
|
104
|
+
resolvedProperty = resolvePropertyEnum(resultProperty);
|
|
105
|
+
} else {
|
|
106
|
+
resolvedProperty = resultProperty;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (resolvedProperty?.propertyConfig && !isDefaultFieldConfigId(resolvedProperty.propertyConfig)) {
|
|
110
|
+
const cmsFields = rest.propertyConfigs;
|
|
111
|
+
if (!cmsFields && !ignoreMissingFields) {
|
|
112
|
+
throw Error(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property configs were provided. Use the property 'propertyConfigs' in your app config to provide them`);
|
|
113
|
+
}
|
|
114
|
+
const customField: PropertyConfig | undefined = cmsFields?.[resolvedProperty.propertyConfig];
|
|
115
|
+
if (!customField) {
|
|
116
|
+
console.warn(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property config with that key was found. Check the 'propertyConfigs' in your app config`)
|
|
117
|
+
return resolvedProperty;
|
|
118
|
+
}
|
|
119
|
+
if (customField.property) {
|
|
120
|
+
const { propertyConfig: _unused, ...restConfigProperty } = customField.property;
|
|
121
|
+
const customFieldProperty = resolveProperty({
|
|
122
|
+
property: { name: "", ...restConfigProperty } as Property,
|
|
123
|
+
ignoreMissingFields,
|
|
124
|
+
...rest
|
|
125
|
+
});
|
|
126
|
+
if (customFieldProperty) {
|
|
127
|
+
resolvedProperty = mergeDeep(customFieldProperty, resolvedProperty);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return resolvedProperty;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resolveRelationProperty(property: RelationProperty, relations: Relation[]) {
|
|
137
|
+
// find the relation by name
|
|
138
|
+
const relation = relations.find((rel) => rel.relationName === property.relationName);
|
|
139
|
+
if (!relation) {
|
|
140
|
+
throw Error(`Relation ${property.relationName} not found`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
...property,
|
|
144
|
+
relation: relation
|
|
145
|
+
} as RelationProperty;
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve enum aliases for a string or number property
|
|
151
|
+
* @param property
|
|
152
|
+
*/
|
|
153
|
+
export function resolvePropertyEnum(property: StringProperty | NumberProperty): StringProperty | NumberProperty {
|
|
154
|
+
if (typeof property.enum === "object") {
|
|
155
|
+
return {
|
|
156
|
+
...property,
|
|
157
|
+
enum: enumToObjectEntries(property.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return property as StringProperty | NumberProperty;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve enums and arrays for properties
|
|
165
|
+
* @param properties
|
|
166
|
+
* @param value
|
|
167
|
+
*/
|
|
168
|
+
export function resolveProperties<M extends Record<string, any>>({
|
|
169
|
+
propertyKey,
|
|
170
|
+
properties,
|
|
171
|
+
ignoreMissingFields,
|
|
172
|
+
...props
|
|
173
|
+
}: {
|
|
174
|
+
propertyKey?: string,
|
|
175
|
+
properties: Properties,
|
|
176
|
+
values?: Partial<M>,
|
|
177
|
+
previousValues?: Partial<M>,
|
|
178
|
+
path?: string,
|
|
179
|
+
entityId?: string | number,
|
|
180
|
+
index?: number,
|
|
181
|
+
propertyConfigs?: Record<string, PropertyConfig>;
|
|
182
|
+
ignoreMissingFields?: boolean;
|
|
183
|
+
authController: AuthController;
|
|
184
|
+
}): Properties {
|
|
185
|
+
return Object.entries<Property>(properties as Record<string, Property>)
|
|
186
|
+
.map(([key, property]) => {
|
|
187
|
+
const childResolvedProperty = resolveProperty({
|
|
188
|
+
propertyKey: propertyKey ? `${propertyKey}.${key}` : undefined,
|
|
189
|
+
property: property,
|
|
190
|
+
ignoreMissingFields,
|
|
191
|
+
...props
|
|
192
|
+
});
|
|
193
|
+
if (!childResolvedProperty) return {};
|
|
194
|
+
return {
|
|
195
|
+
[key]: childResolvedProperty
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((a) => a !== null)
|
|
199
|
+
.reduce((a, b) => ({ ...a, ...b }), {}) as Properties;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function resolveArrayProperties<M>({
|
|
203
|
+
propertyKey,
|
|
204
|
+
property,
|
|
205
|
+
ignoreMissingFields = false,
|
|
206
|
+
...props
|
|
207
|
+
}: {
|
|
208
|
+
propertyKey?: string,
|
|
209
|
+
property: ArrayProperty,
|
|
210
|
+
values?: Partial<M>,
|
|
211
|
+
previousValues?: Partial<M>,
|
|
212
|
+
path?: string,
|
|
213
|
+
entityId?: string | number,
|
|
214
|
+
index?: number,
|
|
215
|
+
propertyConfigs?: Record<string, PropertyConfig>;
|
|
216
|
+
ignoreMissingFields?: boolean;
|
|
217
|
+
authController: AuthController;
|
|
218
|
+
}): Property[] {
|
|
219
|
+
const propertyValue = propertyKey ? getIn(props.values, propertyKey) : undefined;
|
|
220
|
+
|
|
221
|
+
if (property.of) {
|
|
222
|
+
if (Array.isArray(property.of)) {
|
|
223
|
+
return property.of.map((p, index) => {
|
|
224
|
+
return resolveProperty({
|
|
225
|
+
propertyKey: `${propertyKey}.${index}`,
|
|
226
|
+
property: p as Property,
|
|
227
|
+
ignoreMissingFields,
|
|
228
|
+
...props,
|
|
229
|
+
index,
|
|
230
|
+
});
|
|
231
|
+
}) as Property[];
|
|
232
|
+
} else {
|
|
233
|
+
const of = property.of;
|
|
234
|
+
const resolvedProperties = getArrayResolvedProperties({
|
|
235
|
+
propertyValue,
|
|
236
|
+
propertyKey,
|
|
237
|
+
property,
|
|
238
|
+
ignoreMissingFields,
|
|
239
|
+
...props
|
|
240
|
+
});
|
|
241
|
+
const {
|
|
242
|
+
values,
|
|
243
|
+
previousValues,
|
|
244
|
+
...rest
|
|
245
|
+
} = props;
|
|
246
|
+
const ofProperty = resolveProperty({ // we don't want to pass the values of the parent entity
|
|
247
|
+
property: of,
|
|
248
|
+
ignoreMissingFields,
|
|
249
|
+
...rest
|
|
250
|
+
});
|
|
251
|
+
if (!ofProperty && !ignoreMissingFields)
|
|
252
|
+
throw Error("When using a property builder as the 'of' prop of an ArrayProperty, you must return a valid child property")
|
|
253
|
+
return resolvedProperties;
|
|
254
|
+
}
|
|
255
|
+
} else if (property.oneOf) {
|
|
256
|
+
const typeField = property.oneOf?.typeField ?? DEFAULT_ONE_OF_TYPE;
|
|
257
|
+
const resolvedProperties: Property[] = Array.isArray(propertyValue)
|
|
258
|
+
? propertyValue.map((v, index) => {
|
|
259
|
+
const type = v && v[typeField];
|
|
260
|
+
const childProperty = property.oneOf?.properties[type];
|
|
261
|
+
if (!type || !childProperty) return null;
|
|
262
|
+
return resolveProperty({
|
|
263
|
+
propertyKey: `${propertyKey}.${index}`,
|
|
264
|
+
property: childProperty,
|
|
265
|
+
ignoreMissingFields,
|
|
266
|
+
...props
|
|
267
|
+
});
|
|
268
|
+
}).filter(e => Boolean(e)) as Property[]
|
|
269
|
+
: [];
|
|
270
|
+
return resolvedProperties;
|
|
271
|
+
} else if (!property.Field) {
|
|
272
|
+
throw Error(`The array property (${propertyKey}) needs to declare an 'of' or a 'oneOf' property, or provide a custom \`Field\` component`);
|
|
273
|
+
} else {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function getArrayResolvedProperties({
|
|
280
|
+
propertyKey,
|
|
281
|
+
propertyValue,
|
|
282
|
+
property,
|
|
283
|
+
...props
|
|
284
|
+
}: {
|
|
285
|
+
propertyValue: unknown,
|
|
286
|
+
propertyKey?: string,
|
|
287
|
+
property: ArrayProperty,
|
|
288
|
+
ignoreMissingFields: boolean,
|
|
289
|
+
values?: object;
|
|
290
|
+
previousValues?: object;
|
|
291
|
+
path?: string;
|
|
292
|
+
entityId?: string | number;
|
|
293
|
+
index?: number;
|
|
294
|
+
propertyConfigs?: Record<string, PropertyConfig>;
|
|
295
|
+
authController: AuthController;
|
|
296
|
+
}) {
|
|
297
|
+
|
|
298
|
+
const of = property.of;
|
|
299
|
+
if (!of)
|
|
300
|
+
throw Error(
|
|
301
|
+
`Trying to resolve an array property (${propertyKey}) without providing an 'of' property`
|
|
302
|
+
)
|
|
303
|
+
return Array.isArray(propertyValue)
|
|
304
|
+
? propertyValue.map((v: unknown, index: number) => {
|
|
305
|
+
return resolveProperty({
|
|
306
|
+
propertyKey: `${propertyKey}.${index}`,
|
|
307
|
+
property: Array.isArray(of) ? of[index] : of,
|
|
308
|
+
...props,
|
|
309
|
+
index
|
|
310
|
+
});
|
|
311
|
+
}).filter(e => Boolean(e)) as Property[]
|
|
312
|
+
: [];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined {
|
|
316
|
+
if (typeof input === "object") {
|
|
317
|
+
return Object.entries(input).map(([id, value]) =>
|
|
318
|
+
(typeof value === "string"
|
|
319
|
+
? {
|
|
320
|
+
id,
|
|
321
|
+
label: value
|
|
322
|
+
}
|
|
323
|
+
: value));
|
|
324
|
+
} else if (Array.isArray(input)) {
|
|
325
|
+
return input as EnumValueConfig[];
|
|
326
|
+
} else {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function resolveEntityView(entityView: string | EntityCustomView<any>, contextEntityViews?: EntityCustomView<any>[]): EntityCustomView<any> | undefined {
|
|
332
|
+
if (typeof entityView === "string") {
|
|
333
|
+
return contextEntityViews?.find((entry) => entry.key === entityView);
|
|
334
|
+
} else {
|
|
335
|
+
return entityView;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function resolveEntityAction<M extends Record<string, any>>(
|
|
340
|
+
entityAction: string | EntityAction<M>,
|
|
341
|
+
contextEntityActions?: EntityAction<M>[]
|
|
342
|
+
): EntityAction<M> | undefined {
|
|
343
|
+
if (typeof entityAction === "string") {
|
|
344
|
+
return contextEntityActions?.find((entry) => entry.key === entityAction);
|
|
345
|
+
} else {
|
|
346
|
+
return entityAction;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function resolvedSelectedEntityView<M extends Record<string, any>>(
|
|
351
|
+
customViews: (string | EntityCustomView<M>)[] | undefined,
|
|
352
|
+
customizationController: CustomizationController,
|
|
353
|
+
selectedTab?: string,
|
|
354
|
+
canEdit?: boolean,
|
|
355
|
+
) {
|
|
356
|
+
const resolvedEntityViews = customViews ? customViews
|
|
357
|
+
.map(e => resolveEntityView(e, customizationController.entityViews))
|
|
358
|
+
.filter((e): e is EntityCustomView<M> => Boolean(e))
|
|
359
|
+
// .filter((e) => canEdit || !e.includeActions)
|
|
360
|
+
: [];
|
|
361
|
+
|
|
362
|
+
const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
|
|
363
|
+
const selectedSecondaryForm = customViews
|
|
364
|
+
&& resolvedEntityViews.filter(e => e.includeActions).find(e => e.key === selectedTab);
|
|
365
|
+
return {
|
|
366
|
+
resolvedEntityViews,
|
|
367
|
+
selectedEntityView,
|
|
368
|
+
selectedSecondaryForm
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function getSubcollections<M extends Record<string, any> = any>(collection: EntityCollection<M>) {
|
|
373
|
+
const subcollections: EntityCollection<any>[] = [];
|
|
374
|
+
subcollections.push(...(collection.subcollections?.() ?? []));
|
|
375
|
+
subcollections.push(...((collection.relations ?? [])
|
|
376
|
+
.filter((rel) => rel.cardinality === "many")
|
|
377
|
+
.map(relation => {
|
|
378
|
+
const targetCollection = relation.target();
|
|
379
|
+
const overrides = relation.overrides;
|
|
380
|
+
return overrides ? mergeDeep(targetCollection, overrides) : targetCollection;
|
|
381
|
+
}) ?? []));
|
|
382
|
+
return subcollections;
|
|
383
|
+
}
|