@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,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,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
|
+
}
|