@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,126 @@
1
+ import {
2
+ DefaultSelectedViewBuilder,
3
+ DefaultSelectedViewParams,
4
+ EntityCollection,
5
+ Properties,
6
+ Property,
7
+ } from "@rebasepro/types";
8
+ import { isPropertyBuilder } from "./entities";
9
+
10
+ export function sortProperties<M extends Record<string, any>>(properties: Properties, propertiesOrder?: string[]): Properties {
11
+ try {
12
+ const propertiesKeys = Object.keys(properties);
13
+ // If no propertiesOrder, just use the original keys order
14
+ if (!propertiesOrder || propertiesOrder.length === 0) {
15
+ return propertiesKeys
16
+ .map((key) => {
17
+ const property = properties[key] as Property;
18
+ if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
19
+ return ({
20
+ [key]: {
21
+ ...property,
22
+ properties: sortProperties(property.properties, property.propertiesOrder)
23
+ }
24
+ });
25
+ } else {
26
+ return ({ [key]: property });
27
+ }
28
+ })
29
+ .reduce((a: Properties, b: Properties) => ({ ...a, ...b }), {}) as Properties;
30
+ }
31
+
32
+ // Filter propertiesOrder to only include TOP-LEVEL property keys that exist
33
+ // (ignore nested keys like "data.mode" - they are for column ordering, not property filtering)
34
+ const validOrderKeys = (propertiesOrder as string[]).filter(key => {
35
+ // Only include top-level keys (no dots) that exist in properties
36
+ return !key.includes(".") && properties[key];
37
+ });
38
+
39
+ // Track which properties we've processed
40
+ const processedKeys = new Set<string>(validOrderKeys);
41
+
42
+ // Build result starting with ordered properties
43
+ const orderedResult = validOrderKeys
44
+ .map((key) => {
45
+ const property = properties[key] as Property;
46
+ if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
47
+ return ({
48
+ [key]: {
49
+ ...property,
50
+ properties: sortProperties(property.properties, property.propertiesOrder)
51
+ }
52
+ });
53
+ } else {
54
+ return ({ [key]: property });
55
+ }
56
+ })
57
+ .reduce((a: any, b: any) => ({ ...a, ...b }), {}) as Properties;
58
+
59
+ // Append any properties that were NOT in propertiesOrder (so they don't disappear!)
60
+ const missingProperties = propertiesKeys
61
+ .filter(key => !processedKeys.has(key))
62
+ .map((key) => {
63
+ const property = properties[key] as Property;
64
+ if (!isPropertyBuilder(property) && property?.type === "map" && property.properties) {
65
+ return ({
66
+ [key]: {
67
+ ...property,
68
+ properties: sortProperties(property.properties, property.propertiesOrder)
69
+ }
70
+ });
71
+ } else {
72
+ return ({ [key]: property });
73
+ }
74
+ })
75
+ .reduce((a: any, b: any) => ({ ...a, ...b }), {}) as Properties;
76
+
77
+ return { ...orderedResult, ...missingProperties };
78
+ } catch (e) {
79
+ console.error("Error sorting properties", e);
80
+ return properties;
81
+ }
82
+ }
83
+
84
+ export function resolveDefaultSelectedView(
85
+ defaultSelectedView: string | DefaultSelectedViewBuilder | undefined,
86
+ params: DefaultSelectedViewParams
87
+ ) {
88
+ if (!defaultSelectedView) {
89
+ return undefined;
90
+ } else if (typeof defaultSelectedView === "string") {
91
+ return defaultSelectedView;
92
+ } else {
93
+ return defaultSelectedView(params);
94
+ }
95
+ }
96
+
97
+
98
+
99
+ export function getLocalChangesBackup(collection: EntityCollection) {
100
+ if (!collection.localChangesBackup) {
101
+ return "manual_apply";
102
+ }
103
+
104
+ return collection.localChangesBackup;
105
+ }
106
+
107
+ /**
108
+ * Returns the primary keys for an entity collection by inspecting the properties
109
+ * and finding any properties with `isId`.
110
+ * Fallbacks to `["id"]` if no properties are marked as `isId: true`.
111
+ * @param collection
112
+ */
113
+ export function getPrimaryKeys<M extends Record<string, any>>(collection: EntityCollection<M>): Extract<keyof M, string>[] {
114
+ const properties = collection.properties;
115
+ if (!properties) {
116
+ return ["id"] as Extract<keyof M, string>[];
117
+ }
118
+ const ids = Object.entries(properties)
119
+ .filter(([key, prop]) => typeof prop === "object" && prop !== null && "isId" in prop && Boolean(prop.isId))
120
+ .map(([key]) => key);
121
+
122
+ if (ids.length > 0) {
123
+ return ids as Extract<keyof M, string>[];
124
+ }
125
+ return ["id"] as Extract<keyof M, string>[];
126
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_ONE_OF_TYPE = "type"
2
+ export const DEFAULT_ONE_OF_VALUE = "value"
@@ -0,0 +1,348 @@
1
+ import jsonLogic from "json-logic-js";
2
+ import {
3
+ ArrayProperty,
4
+ AuthController,
5
+ ConditionContext,
6
+ EnumValueConfig,
7
+ JsonLogicRule,
8
+ NumberProperty,
9
+ PropertyConditions,
10
+ Property,
11
+ ReferenceProperty,
12
+ StringProperty
13
+ } from "@rebasepro/types";
14
+
15
+ /**
16
+ * Access a nested property from an object via dot notation.
17
+ */
18
+ function getIn(obj: Record<string, unknown> | unknown, path: string): unknown {
19
+ if (!obj || !path) return undefined;
20
+ return path.split('.').reduce((acc: unknown, part: string) => acc && (acc as Record<string, unknown>)[part], obj);
21
+ }
22
+
23
+ let operationsRegistered = false;
24
+
25
+ /**
26
+ * Register custom JSON Logic operations for Rebase.
27
+ * Call this once at app initialization.
28
+ */
29
+ export function registerConditionOperations(): void {
30
+ if (operationsRegistered) return;
31
+
32
+ // Check if user has a specific role by ID
33
+ jsonLogic.add_operation("hasRole", function (this: ConditionContext, roleId: string) {
34
+ return this?.user?.roles?.includes(roleId) ?? false;
35
+ });
36
+
37
+ // Check if user has any of the specified roles
38
+ jsonLogic.add_operation("hasAnyRole", function (this: ConditionContext, roleIds: string[]) {
39
+ if (!this?.user?.roles || !Array.isArray(roleIds)) return false;
40
+ return roleIds.some(role => this.user.roles.includes(role));
41
+ });
42
+
43
+ // Check if a timestamp is today
44
+ jsonLogic.add_operation("isToday", (timestamp: number) => {
45
+ if (!timestamp) return false;
46
+ const date = new Date(timestamp);
47
+ const today = new Date();
48
+ return date.getFullYear() === today.getFullYear() &&
49
+ date.getMonth() === today.getMonth() &&
50
+ date.getDate() === today.getDate();
51
+ });
52
+
53
+ // Check if a timestamp is in the past
54
+ jsonLogic.add_operation("isPast", (timestamp: number) => {
55
+ if (!timestamp) return false;
56
+ return timestamp < Date.now();
57
+ });
58
+
59
+ // Check if a timestamp is in the future
60
+ jsonLogic.add_operation("isFuture", (timestamp: number) => {
61
+ if (!timestamp) return false;
62
+ return timestamp > Date.now();
63
+ });
64
+
65
+ operationsRegistered = true;
66
+ }
67
+
68
+ /**
69
+ * Evaluate a JSON Logic rule against the given context.
70
+ */
71
+ export function evaluateCondition(rule: JsonLogicRule, context: ConditionContext): unknown {
72
+ // Ensure operations are registered
73
+ registerConditionOperations();
74
+ return jsonLogic.apply(rule, context);
75
+ }
76
+
77
+ /**
78
+ * Convert a value to a format suitable for JSON Logic evaluation.
79
+ * Specifically handles Date objects by converting them to Unix timestamps.
80
+ */
81
+ function serializeValueForConditions(value: unknown): unknown {
82
+ if (value === null || value === undefined) {
83
+ return value;
84
+ }
85
+
86
+ // Handle Date objects
87
+ if (value instanceof Date) {
88
+ return value.getTime();
89
+ }
90
+
91
+ // Handle Firestore Timestamp-like objects (have toDate or toMillis)
92
+ if (typeof (value as { toMillis?: () => number })?.toMillis === "function") {
93
+ return (value as { toMillis: () => number }).toMillis();
94
+ }
95
+ if (typeof (value as { toDate?: () => Date })?.toDate === "function") {
96
+ return (value as { toDate: () => Date }).toDate().getTime();
97
+ }
98
+
99
+ // Handle arrays recursively
100
+ if (Array.isArray(value)) {
101
+ return value.map(serializeValueForConditions);
102
+ }
103
+
104
+ // Handle plain objects recursively
105
+ if (typeof value === "object") {
106
+ const result: Record<string, unknown> = {};
107
+ for (const key of Object.keys(value as Record<string, unknown>)) {
108
+ result[key] = serializeValueForConditions((value as Record<string, unknown>)[key]);
109
+ }
110
+ return result;
111
+ }
112
+
113
+ return value;
114
+ }
115
+
116
+ /**
117
+ * Build a ConditionContext from the current property resolution context.
118
+ */
119
+ export function buildConditionContext(params: {
120
+ propertyKey?: string;
121
+ values?: Record<string, unknown>;
122
+ previousValues?: Record<string, unknown>;
123
+ path: string;
124
+ entityId?: string;
125
+ index?: number;
126
+ authController: AuthController;
127
+ }): ConditionContext {
128
+ const {
129
+ propertyKey,
130
+ values,
131
+ previousValues,
132
+ path,
133
+ entityId,
134
+ index,
135
+ authController
136
+ } = params;
137
+
138
+ const user = authController.user;
139
+ const serializedValues = serializeValueForConditions(values ?? {});
140
+ const serializedPreviousValues = serializeValueForConditions(previousValues ?? values ?? {});
141
+
142
+ return {
143
+ values: serializedValues as Record<string, unknown>,
144
+ previousValues: serializedPreviousValues as Record<string, unknown>,
145
+ propertyValue: propertyKey ? getIn(serializedValues, propertyKey) : undefined,
146
+ path,
147
+ entityId,
148
+ isNew: !entityId,
149
+ index,
150
+ user: {
151
+ uid: user?.uid ?? "",
152
+ email: user?.email ?? null,
153
+ displayName: user?.displayName ?? null,
154
+ photoURL: user?.photoURL ?? null,
155
+ roles: user?.roles ?? []
156
+ },
157
+ now: Date.now()
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
163
+ */
164
+ export function applyPropertyConditions(
165
+ property: Property,
166
+ context: ConditionContext
167
+ ): Property {
168
+ const { conditions } = property;
169
+ if (!conditions) return property;
170
+
171
+ let result = { ...property };
172
+
173
+ // ═══════════════════════════════════════════════════════════════════════
174
+ // FIELD STATE CONDITIONS
175
+ // ═══════════════════════════════════════════════════════════════════════
176
+
177
+ // Evaluate disabled condition
178
+ if (conditions.disabled) {
179
+ const isDisabled = evaluateCondition(conditions.disabled, context);
180
+ if (isDisabled) {
181
+ result.disabled = {
182
+ clearOnDisabled: conditions.clearOnDisabled ?? false,
183
+ disabledMessage: conditions.disabledMessage,
184
+ hidden: false
185
+ };
186
+ }
187
+ }
188
+
189
+ // Evaluate hidden condition
190
+ if (conditions.hidden) {
191
+ const isHidden = evaluateCondition(conditions.hidden, context);
192
+ if (isHidden) {
193
+ result.disabled = {
194
+ ...(typeof result.disabled === "object" ? result.disabled : {}),
195
+ hidden: true,
196
+ clearOnDisabled: conditions.clearOnDisabled ?? false
197
+ };
198
+ }
199
+ }
200
+
201
+ // Evaluate readOnly condition
202
+ if (conditions.readOnly) {
203
+ const isReadOnly = evaluateCondition(conditions.readOnly, context);
204
+ if (isReadOnly) {
205
+ result.readOnly = true;
206
+ }
207
+ }
208
+
209
+ // ═══════════════════════════════════════════════════════════════════════
210
+ // VALIDATION CONDITIONS
211
+ // ═══════════════════════════════════════════════════════════════════════
212
+
213
+ // Evaluate required condition
214
+ if (conditions.required !== undefined) {
215
+ const isRequired = evaluateCondition(conditions.required, context) as boolean;
216
+ result.validation = {
217
+ ...result.validation,
218
+ required: isRequired as boolean | undefined,
219
+ requiredMessage: conditions.requiredMessage
220
+ };
221
+ }
222
+
223
+ // ═══════════════════════════════════════════════════════════════════════
224
+ // VALUE CONDITIONS
225
+ // ═══════════════════════════════════════════════════════════════════════
226
+
227
+ // Apply default value for new entities
228
+ if (context.isNew && conditions.defaultValue !== undefined) {
229
+ result.defaultValue = evaluateCondition(conditions.defaultValue, context) as Property["defaultValue"];
230
+ }
231
+
232
+ // ═══════════════════════════════════════════════════════════════════════
233
+ // ENUM CONDITIONS
234
+ // ═══════════════════════════════════════════════════════════════════════
235
+
236
+ if ("enumValues" in result && result.enumValues && (conditions.enumConditions || conditions.allowedEnumValues || conditions.excludedEnumValues)) {
237
+ (result as Record<string, unknown>).enumValues = applyEnumConditions(
238
+ result.enumValues as EnumValueConfig[],
239
+ conditions,
240
+ context
241
+ );
242
+ }
243
+
244
+ // ═══════════════════════════════════════════════════════════════════════
245
+ // REFERENCE CONDITIONS
246
+ // ═══════════════════════════════════════════════════════════════════════
247
+
248
+ if (result.type === "reference") {
249
+ if (conditions.referencePath) {
250
+ (result as ReferenceProperty).path = evaluateCondition(conditions.referencePath, context) as string;
251
+ }
252
+ if (conditions.referenceFilter) {
253
+ (result as ReferenceProperty).forceFilter = evaluateCondition(conditions.referenceFilter, context) as ReferenceProperty["forceFilter"];
254
+ }
255
+ }
256
+
257
+ // ═══════════════════════════════════════════════════════════════════════
258
+ // ARRAY CONDITIONS
259
+ // ═══════════════════════════════════════════════════════════════════════
260
+
261
+ if (result.type === "array") {
262
+ if (conditions.canAddElements !== undefined) {
263
+ (result as ArrayProperty).canAddElements = evaluateCondition(conditions.canAddElements, context) as boolean;
264
+ }
265
+ if (conditions.sortable !== undefined) {
266
+ (result as ArrayProperty).sortable = evaluateCondition(conditions.sortable, context) as boolean;
267
+ }
268
+ }
269
+
270
+ return result;
271
+ }
272
+
273
+ /**
274
+ * Convert an object with numeric keys back to an array.
275
+ * Firestore stores arrays as {"0": "a", "1": "b"} to avoid nested arrays.
276
+ */
277
+ function objectToArray(obj: unknown): string[] {
278
+ if (Array.isArray(obj)) return obj.map(String);
279
+ if (obj && typeof obj === "object") {
280
+ const keys = Object.keys(obj);
281
+ if (keys.length > 0 && keys.every(k => !isNaN(Number(k)))) {
282
+ return keys
283
+ .sort((a, b) => Number(a) - Number(b))
284
+ .map(k => (obj as Record<string, unknown>)[k])
285
+ .filter((v): v is string => typeof v === "string" || typeof v === "number")
286
+ .map(String);
287
+ }
288
+ }
289
+ return [];
290
+ }
291
+
292
+ /**
293
+ * Apply enum-specific conditions to filter and modify enum values.
294
+ */
295
+ function applyEnumConditions(
296
+ enumValues: EnumValueConfig[],
297
+ conditions: PropertyConditions,
298
+ context: ConditionContext
299
+ ): EnumValueConfig[] {
300
+ let result = [...enumValues];
301
+
302
+ // Apply allowedEnumValues filter
303
+ if (conditions.allowedEnumValues) {
304
+ const allowed = evaluateCondition(conditions.allowedEnumValues, context);
305
+ // Handle both array format and object-with-numeric-keys format (Firestore workaround)
306
+ const allowedArray = objectToArray(allowed);
307
+ if (allowedArray.length > 0) {
308
+ result = result.filter(ev => allowedArray.includes(String(ev.id)));
309
+ }
310
+ }
311
+
312
+ // Apply excludedEnumValues filter
313
+ if (conditions.excludedEnumValues) {
314
+ const excluded = evaluateCondition(conditions.excludedEnumValues, context);
315
+ // Handle both array format and object-with-numeric-keys format
316
+ const excludedArray = objectToArray(excluded);
317
+ if (excludedArray.length > 0) {
318
+ result = result.filter(ev => !excludedArray.includes(String(ev.id)));
319
+ }
320
+ }
321
+
322
+ // Apply individual enum conditions
323
+ if (conditions.enumConditions) {
324
+ result = result
325
+ .map(ev => {
326
+ const evConditions = conditions.enumConditions?.[ev.id];
327
+ if (!evConditions) return ev;
328
+
329
+ // Check hidden condition first
330
+ if (evConditions.hidden && evaluateCondition(evConditions.hidden, context)) {
331
+ return null; // Will be filtered out
332
+ }
333
+
334
+ // Check disabled condition
335
+ if (evConditions.disabled && evaluateCondition(evConditions.disabled, context)) {
336
+ return {
337
+ ...ev,
338
+ disabled: true
339
+ };
340
+ }
341
+
342
+ return ev;
343
+ })
344
+ .filter((ev): ev is EnumValueConfig => ev !== null);
345
+ }
346
+
347
+ return result;
348
+ }
@@ -0,0 +1 @@
1
+ export const defaultDateFormat = "MMMM dd, yyyy, HH:mm:ss";
@@ -0,0 +1,212 @@
1
+ import {
2
+ DataType,
3
+ Entity,
4
+ EntityReference,
5
+ EntityRelation,
6
+ EntityStatus,
7
+ EntityValues,
8
+ Properties,
9
+ Property,
10
+ } from "@rebasepro/types";
11
+ import { DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "./common";
12
+ import { mergeDeep } from "./objects";
13
+
14
+ export function isReadOnly(property: Property): boolean {
15
+ if (property.readOnly)
16
+ return true;
17
+ if (property.type === "date") {
18
+ if (property.autoValue)
19
+ return true;
20
+ }
21
+ if (property.type === "reference") {
22
+ return !property.path && !property.Field;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ export function isHidden(property: Property): boolean {
28
+ return typeof property.disabled === "object" && Boolean(property.disabled.hidden);
29
+ }
30
+
31
+ export function isPropertyBuilder(property?: Property) {
32
+ return typeof property?.dynamicProps === "function";
33
+ }
34
+
35
+ export function getDefaultValuesFor<M extends Record<string, any>>(properties: Properties): Partial<EntityValues<M>> {
36
+ if (!properties) return {};
37
+ return Object.entries(properties)
38
+ .map(([key, property]) => {
39
+ if (!property) return {};
40
+ const value = getDefaultValueFor(property);
41
+ return value === undefined ? {} : { [key]: value };
42
+ })
43
+ .reduce((a, b) => ({ ...a, ...b }), {}) as EntityValues<M>;
44
+ }
45
+
46
+ export function getDefaultValueFor(property?: Property) {
47
+ if (!property) return undefined;
48
+ if (isPropertyBuilder(property)) return undefined;
49
+ if (property.defaultValue || property.defaultValue === null) {
50
+ return property.defaultValue;
51
+ } else if (property.type === "map" && property.properties) {
52
+ const defaultValuesFor = getDefaultValuesFor(property.properties as Properties);
53
+ if (Object.keys(defaultValuesFor).length === 0) return undefined;
54
+ return defaultValuesFor;
55
+ } else {
56
+ return getDefaultValueFortype(property.type);
57
+ }
58
+ }
59
+
60
+ export function getDefaultValueFortype(type: DataType) {
61
+ if (type === "string") {
62
+ return null;
63
+ } else if (type === "number") {
64
+ return null;
65
+ } else if (type === "boolean") {
66
+ return false;
67
+ } else if (type === "date") {
68
+ return null;
69
+ } else if (type === "array") {
70
+ return [];
71
+ } else if (type === "map") {
72
+ return {};
73
+ } else {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Update the automatic values in an entity before save
80
+ * @group Datasource
81
+ */
82
+ export function updateDateAutoValues<M extends Record<string, any>>({
83
+ inputValues,
84
+ properties,
85
+ status,
86
+ timestampNowValue
87
+ }:
88
+ {
89
+ inputValues: Partial<EntityValues<M>>,
90
+ properties: Properties,
91
+ status: EntityStatus,
92
+ timestampNowValue: unknown
93
+ }): EntityValues<M> {
94
+ return traverseValuesProperties(
95
+ inputValues,
96
+ properties,
97
+ (inputValue, property) => {
98
+ if (property.type === "date") {
99
+ if (status === "existing" && property.autoValue === "on_update") {
100
+ return timestampNowValue;
101
+ } else if ((status === "new" || status === "copy") &&
102
+ (property.autoValue === "on_update" || property.autoValue === "on_create")) {
103
+ return timestampNowValue;
104
+ } else {
105
+ return inputValue;
106
+ }
107
+ } else {
108
+ return inputValue;
109
+ }
110
+ }
111
+ ) ?? {} as M;
112
+ }
113
+
114
+ /**
115
+ * Add missing required fields, expected in the collection, to the values of an entity
116
+ * @param values
117
+ * @param properties
118
+ * @group Datasource
119
+ */
120
+ export function sanitizeData<M extends Record<string, any>>
121
+ (
122
+ values: EntityValues<M>,
123
+ properties: Properties
124
+ ) {
125
+ const result = values as Record<string, unknown>;
126
+ Object.entries(properties)
127
+ .forEach(([key, property]) => {
128
+ if (values && values[key] !== undefined) result[key] = values[key];
129
+ else if ((property as Property).validation?.required) result[key] = null;
130
+ });
131
+ return result;
132
+ }
133
+
134
+ export function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M>): EntityReference {
135
+ if (typeof entity.id !== "string")
136
+ throw new Error("Only string IDs are supported in references");
137
+ return new EntityReference({
138
+ id: entity.id,
139
+ path: entity.path,
140
+ datasource: entity.datasource,
141
+ databaseId: entity.databaseId
142
+ });
143
+ }
144
+
145
+ export function getRelationFrom<M extends Record<string, any>>(entity: Entity<M>): EntityRelation {
146
+ return new EntityRelation(entity.id, entity.path);
147
+ }
148
+
149
+ export function traverseValuesProperties<M extends Record<string, any>>(
150
+ inputValues: Partial<EntityValues<M>>,
151
+ properties: Properties,
152
+ operation: (value: unknown, property: Property) => unknown
153
+ ): EntityValues<M> | undefined {
154
+ // Handle null/undefined inputValues - use empty object as base for mergeDeep
155
+ const safeInputValues = inputValues ?? {};
156
+
157
+ const updatedValues = Object.entries(properties)
158
+ .map(([key, property]) => {
159
+ const inputValue = safeInputValues && (safeInputValues)[key];
160
+ const updatedValue = traverseValueProperty(inputValue, property as Property, operation);
161
+ if (updatedValue === null) return null;
162
+ if (updatedValue === undefined) return undefined;
163
+ return ({ [key]: updatedValue });
164
+ })
165
+ .reduce((a, b) => ({ ...a, ...b }), {}) as EntityValues<M>;
166
+ // Use mergeDeep to preserve class instances like EntityReference, GeoPoint
167
+ const result = mergeDeep(safeInputValues, updatedValues);
168
+ if (!result || Object.keys(result).length === 0) return undefined;
169
+ return result;
170
+ }
171
+
172
+ export function traverseValueProperty(inputValue: unknown,
173
+ property: Property,
174
+ operation: (value: unknown, property: Property) => unknown): unknown {
175
+
176
+ let value;
177
+ if (property.type === "map" && property.properties) {
178
+ value = traverseValuesProperties(inputValue as Partial<Record<string, unknown>>, property.properties, operation);
179
+ } else if (property.type === "array") {
180
+ const of = property.of;
181
+ if (of && Array.isArray(inputValue) && !Array.isArray(of)) {
182
+ value = inputValue.map((e) => traverseValueProperty(e, of, operation));
183
+ } else if (of && Array.isArray(inputValue) && Array.isArray(of)) {
184
+ value = inputValue.map((e, i) => {
185
+ if (i < of.length)
186
+ return traverseValueProperty(e, of[i], operation);
187
+ return null
188
+ }).filter(Boolean);
189
+ } else if (property.oneOf && Array.isArray(inputValue)) {
190
+ const typeField = property.oneOf?.typeField ?? DEFAULT_ONE_OF_TYPE;
191
+ const valueField = property.oneOf?.valueField ?? DEFAULT_ONE_OF_VALUE;
192
+ value = inputValue.map((e) => {
193
+ if (e === null) return null;
194
+ if (typeof e !== "object") return e;
195
+ const rec = e as Record<string, unknown>;
196
+ const type = rec[typeField] as string;
197
+ const childProperty = property.oneOf?.properties[type];
198
+ if (!type || !childProperty) return e;
199
+ return {
200
+ [typeField]: type,
201
+ [valueField]: traverseValueProperty(rec[valueField], childProperty, operation)
202
+ };
203
+ });
204
+ } else {
205
+ value = inputValue;
206
+ }
207
+ } else {
208
+ value = operation(inputValue, property);
209
+ }
210
+
211
+ return value;
212
+ }