@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/collections/CollectionRegistry.d.ts +48 -0
  4. package/dist/collections/index.d.ts +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.es.js +2380 -0
  7. package/dist/index.es.js.map +1 -0
  8. package/dist/index.umd.js +2379 -0
  9. package/dist/index.umd.js.map +1 -0
  10. package/dist/util/arrays.d.ts +1 -0
  11. package/dist/util/builders.d.ts +64 -0
  12. package/dist/util/callbacks.d.ts +6 -0
  13. package/dist/util/collections.d.ts +11 -0
  14. package/dist/util/common.d.ts +2 -0
  15. package/dist/util/conditions.d.ts +26 -0
  16. package/dist/util/dates.d.ts +1 -0
  17. package/dist/util/entities.d.ts +28 -0
  18. package/dist/util/entity_actions.d.ts +2 -0
  19. package/dist/util/enums.d.ts +3 -0
  20. package/dist/util/fields.d.ts +2 -0
  21. package/dist/util/flatten_object.d.ts +5 -0
  22. package/dist/util/hash.d.ts +1 -0
  23. package/dist/util/index.d.ts +26 -0
  24. package/dist/util/names.d.ts +22 -0
  25. package/dist/util/navigation_from_path.d.ts +29 -0
  26. package/dist/util/navigation_utils.d.ts +31 -0
  27. package/dist/util/objects.d.ts +26 -0
  28. package/dist/util/os.d.ts +2 -0
  29. package/dist/util/parent_references_from_path.d.ts +6 -0
  30. package/dist/util/paths.d.ts +14 -0
  31. package/dist/util/permissions.d.ts +5 -0
  32. package/dist/util/permissions.test.d.ts +1 -0
  33. package/dist/util/plurals.d.ts +16 -0
  34. package/dist/util/references.d.ts +2 -0
  35. package/dist/util/regexp.d.ts +7 -0
  36. package/dist/util/relations.d.ts +12 -0
  37. package/dist/util/resolutions.d.ts +74 -0
  38. package/dist/util/storage.d.ts +24 -0
  39. package/dist/util/strings.d.ts +7 -0
  40. package/package.json +118 -0
  41. package/src/collections/CollectionRegistry.ts +319 -0
  42. package/src/collections/index.ts +1 -0
  43. package/src/index.ts +2 -0
  44. package/src/util/arrays.ts +3 -0
  45. package/src/util/builders.ts +138 -0
  46. package/src/util/callbacks.ts +115 -0
  47. package/src/util/collections.ts +126 -0
  48. package/src/util/common.ts +2 -0
  49. package/src/util/conditions.ts +348 -0
  50. package/src/util/dates.ts +1 -0
  51. package/src/util/entities.ts +212 -0
  52. package/src/util/entity_actions.ts +28 -0
  53. package/src/util/enums.ts +26 -0
  54. package/src/util/fields.ts +28 -0
  55. package/src/util/flatten_object.ts +45 -0
  56. package/src/util/hash.ts +11 -0
  57. package/src/util/index.ts +26 -0
  58. package/src/util/names.ts +30 -0
  59. package/src/util/navigation_from_path.ts +121 -0
  60. package/src/util/navigation_utils.ts +222 -0
  61. package/src/util/objects.ts +376 -0
  62. package/src/util/os.ts +13 -0
  63. package/src/util/parent_references_from_path.ts +57 -0
  64. package/src/util/paths.ts +27 -0
  65. package/src/util/permissions.test.ts +716 -0
  66. package/src/util/permissions.ts +235 -0
  67. package/src/util/plurals.ts +188 -0
  68. package/src/util/references.ts +34 -0
  69. package/src/util/regexp.ts +32 -0
  70. package/src/util/relations.ts +211 -0
  71. package/src/util/resolutions.ts +383 -0
  72. package/src/util/storage.ts +144 -0
  73. 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
+ }