@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,319 @@
1
+ import {
2
+ EntityCollection,
3
+ NumberProperty,
4
+ Properties,
5
+ Property,
6
+ Relation,
7
+ RelationProperty,
8
+ StringProperty
9
+ } from "@rebasepro/types";
10
+ import { deepEqual } from "fast-equals";
11
+
12
+ import { enumToObjectEntries, getSubcollections, removeFunctions, resolveCollectionRelations } from "../util";
13
+ import cloneDeep from "lodash/cloneDeep.js";
14
+
15
+ export class CollectionRegistry {
16
+
17
+ // Normalized runtime layer (used by Data Grid / UI)
18
+ private collectionsByDbPath = new Map<string, EntityCollection>();
19
+ private collectionsBySlug = new Map<string, EntityCollection>();
20
+ private rootCollections: EntityCollection[] = [];
21
+
22
+ // Raw configuration layer (used by Collection Editor AST generator)
23
+ private rawCollectionsByDbPath = new Map<string, EntityCollection>();
24
+ private rawCollectionsBySlug = new Map<string, EntityCollection>();
25
+ private rawRootCollections: EntityCollection[] = [];
26
+
27
+ // Snapshot of raw input for idempotency check — compared BEFORE normalization
28
+ // to avoid the issue where normalization creates new objects that always fail equality.
29
+ private lastRawInputSnapshot: ReturnType<typeof removeFunctions>[] | null = null;
30
+
31
+ constructor(collections?: EntityCollection[]) {
32
+ if (collections) {
33
+ this.registerMultiple(collections);
34
+ }
35
+ }
36
+
37
+ reset() {
38
+ this.collectionsByDbPath.clear();
39
+ this.collectionsBySlug.clear();
40
+ this.rootCollections = [];
41
+
42
+ this.rawCollectionsByDbPath.clear();
43
+ this.rawCollectionsBySlug.clear();
44
+ this.rawRootCollections = [];
45
+ }
46
+
47
+ /**
48
+ * Registers a collection and its subcollections recursively.
49
+ * Returns true if the collections have changed, false otherwise.
50
+ *
51
+ * Idempotent: compares the raw input (before normalization) against a stored
52
+ * snapshot. Only re-normalizes and re-registers when the raw input actually changed.
53
+ * @param collections
54
+ */
55
+ registerMultiple(collections: EntityCollection[]): boolean {
56
+ // Compare raw input BEFORE normalization to detect actual changes.
57
+ // This avoids the old issue where normalization creates new objects
58
+ // that always fail deep-equal even when the source data is identical.
59
+ const rawSnapshot = collections.map(c => removeFunctions(c));
60
+ if (this.lastRawInputSnapshot && deepEqual(this.lastRawInputSnapshot, rawSnapshot)) {
61
+ return false;
62
+ }
63
+
64
+ // Raw input has changed — normalize and register
65
+ this.reset();
66
+
67
+ const normalizedCollections = collections.map(c => this.normalizeCollection({ ...c }));
68
+ normalizedCollections.forEach((c, index) => this.register(c, collections[index]));
69
+
70
+ // Store the snapshot for future comparisons
71
+ this.lastRawInputSnapshot = rawSnapshot;
72
+
73
+ return true;
74
+ }
75
+
76
+ register(collection: EntityCollection, rawCollection?: EntityCollection) {
77
+ const raw = rawCollection ? cloneDeep(rawCollection) : cloneDeep(collection);
78
+
79
+ this.rootCollections.push(collection);
80
+ this.rawRootCollections.push(raw);
81
+
82
+ this._registerRecursively(collection, raw);
83
+ }
84
+
85
+ private _registerRecursively(collection: EntityCollection, rawCollection: EntityCollection) {
86
+ if (this.collectionsByDbPath.has(collection.dbPath)) {
87
+ return;
88
+ }
89
+
90
+ const normalizedCollection = this.normalizeCollection(collection);
91
+ this.collectionsByDbPath.set(normalizedCollection.dbPath, normalizedCollection);
92
+ this.rawCollectionsByDbPath.set(rawCollection.dbPath, rawCollection);
93
+
94
+ if (normalizedCollection.slug) {
95
+ this.collectionsBySlug.set(normalizedCollection.slug, normalizedCollection);
96
+ }
97
+ if (rawCollection.slug) {
98
+ this.rawCollectionsBySlug.set(rawCollection.slug, rawCollection);
99
+ }
100
+
101
+ const subcollections = getSubcollections(collection);
102
+ const rawSubcollections = getSubcollections(rawCollection);
103
+
104
+ if (subcollections && rawSubcollections) {
105
+ subcollections.forEach((subCollection, index) => {
106
+ this._registerRecursively(this.normalizeCollection(subCollection), cloneDeep(rawSubcollections[index]));
107
+ });
108
+ }
109
+ }
110
+
111
+ public normalizeCollection(collection: EntityCollection): EntityCollection {
112
+ const properties: Properties = this.normalizeProperties(collection.properties, collection.relations ?? []);
113
+
114
+ collection.properties = properties;
115
+ return collection;
116
+ }
117
+
118
+ private normalizeProperties(properties: Properties, relations: Relation[]): Properties {
119
+ const newProperties: Properties = {};
120
+ for (const key in properties) {
121
+ newProperties[key] = this.normalizeProperty(properties[key], relations);
122
+ }
123
+ return newProperties;
124
+ }
125
+
126
+ private normalizeProperty(property: Property, relations: Relation[]): Property {
127
+ const newProperty = { ...property };
128
+
129
+ if (newProperty.type === "map" && newProperty.properties) {
130
+ newProperty.properties = this.normalizeProperties(newProperty.properties, relations);
131
+ } else if (newProperty.type === "array") {
132
+ if (newProperty.of) {
133
+ newProperty.of = this.normalizeProperty(newProperty.of, relations);
134
+ } else if (newProperty.oneOf && newProperty.oneOf.properties) {
135
+ newProperty.oneOf.properties = this.normalizeProperties(newProperty.oneOf.properties, relations);
136
+ }
137
+ } else if ((newProperty.type === "string" || newProperty.type === "number") && newProperty.enum) {
138
+ const stringOrNumberProperty = newProperty as StringProperty | NumberProperty;
139
+ if (typeof stringOrNumberProperty.enum === "object" && !Array.isArray(stringOrNumberProperty.enum)) {
140
+ (stringOrNumberProperty as unknown as Record<string, unknown>).enum = enumToObjectEntries(stringOrNumberProperty.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [];
141
+ }
142
+ } else if (newProperty.type === "relation") {
143
+ const relationProperty = newProperty as RelationProperty;
144
+ const relation = relations.find(r => r.relationName === relationProperty.relationName);
145
+ if (relation) {
146
+ // we attach the resolved relation to the property
147
+ (relationProperty as unknown as Record<string, unknown>).relation = relation;
148
+ } else {
149
+ console.warn(`Could not find relation for property with relationName: ${relationProperty.relationName}`);
150
+ }
151
+ }
152
+
153
+ return newProperty;
154
+ }
155
+
156
+ get(path: string): EntityCollection | undefined {
157
+ // First try slug lookup
158
+ const bySlug = this.collectionsBySlug.get(path);
159
+ if (bySlug) return bySlug;
160
+
161
+ // Fallback to dbPath lookup
162
+ return this.collectionsByDbPath.get(path);
163
+ }
164
+
165
+ /**
166
+ * Gets the pristine, un-normalized collection exactly as it was provided.
167
+ * Useful for the AST editor so it doesn't accidentally serialize injected metadata back to disk.
168
+ */
169
+ getRaw(path: string): EntityCollection | undefined {
170
+ const bySlug = this.rawCollectionsBySlug.get(path);
171
+ if (bySlug) return bySlug;
172
+ return this.rawCollectionsByDbPath.get(path);
173
+ }
174
+
175
+ /**
176
+ * Get collection by resolving multi-segment paths through relations
177
+ * e.g., "authors/70/posts" resolves to the posts collection
178
+ */
179
+ getCollectionByPath(collectionPath: string): EntityCollection | undefined {
180
+ // Handle simple single collection path
181
+ if (!collectionPath.includes("/")) {
182
+ return this.get(collectionPath);
183
+ }
184
+
185
+ // Handle multi-segment paths by resolving through relations
186
+ const pathSegments = collectionPath.split("/").filter(p => p);
187
+
188
+ if (pathSegments.length < 3 || pathSegments.length % 2 === 0) {
189
+ throw new Error(`Invalid relation path: ${collectionPath}. Expected format: collection/id/relation or collection/id/relation/id/relation`);
190
+ }
191
+
192
+ // Start with the root collection
193
+ const rootCollectionPath = pathSegments[0];
194
+ let currentCollection = this.get(rootCollectionPath);
195
+
196
+ if (!currentCollection) {
197
+ throw new Error(`Root collection not found: ${rootCollectionPath}`);
198
+ }
199
+
200
+ // Navigate through the path using relations
201
+ for (let i = 2; i < pathSegments.length; i += 2) {
202
+ const relationKey = pathSegments[i];
203
+
204
+ // Get relations for current collection
205
+ const resolvedRelations = resolveCollectionRelations(currentCollection);
206
+ const relation = resolvedRelations[relationKey];
207
+
208
+ if (!relation) {
209
+ throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug || currentCollection.dbPath}'`);
210
+ }
211
+
212
+ // Move to the target collection
213
+ currentCollection = relation.target();
214
+
215
+ // If there are more segments, continue navigation
216
+ if (i + 1 < pathSegments.length) {
217
+ // Skip entity ID segment
218
+ }
219
+ }
220
+
221
+ return currentCollection;
222
+ }
223
+
224
+ getCollections(): EntityCollection[] {
225
+ return Array.from(this.collectionsByDbPath.values());
226
+ }
227
+
228
+ getRawCollections(): EntityCollection[] {
229
+ return Array.from(this.rawCollectionsByDbPath.values());
230
+ }
231
+
232
+ /**
233
+ * Resolves a multi-segment path like "products/123/locales" and returns
234
+ * information about the collections and entity IDs along the path
235
+ */
236
+ resolvePathToCollections(path: string): {
237
+ collections: EntityCollection[],
238
+ entityIds: (string | number)[],
239
+ finalCollection: EntityCollection
240
+ } {
241
+ const pathSegments = path.split("/").filter(p => p);
242
+
243
+ if (pathSegments.length === 0) {
244
+ throw new Error(`Invalid path: ${path}`);
245
+ }
246
+
247
+ if (pathSegments.length % 2 !== 1) {
248
+ throw new Error(`Invalid collection path: ${path}. It must have an odd number of segments.`);
249
+ }
250
+
251
+ const collections: EntityCollection[] = [];
252
+ const entityIds: (string | number)[] = [];
253
+
254
+ // Start with the first collection
255
+ let currentCollection = this.get(pathSegments[0]);
256
+
257
+ if (!currentCollection) {
258
+ throw new Error(`Unknown collection path or slug: ${pathSegments[0]}`);
259
+ }
260
+
261
+ collections.push(currentCollection);
262
+
263
+ // Process the rest of the path in pairs (entityId, subcollectionSlug)
264
+ for (let i = 1; i < pathSegments.length; i += 2) {
265
+ const entityId = pathSegments[i];
266
+ entityIds.push(entityId);
267
+
268
+ if (i + 1 < pathSegments.length) {
269
+ const subcollectionSlug = pathSegments[i + 1];
270
+ const subcollections: EntityCollection[] | undefined = currentCollection.subcollections?.();
271
+ if (!subcollections) {
272
+ throw new Error(`No subcollections found for ${currentCollection.slug || currentCollection.dbPath} in path: ${path}`);
273
+ }
274
+
275
+ const subcollection: EntityCollection | undefined = subcollections.find(c => c.slug === subcollectionSlug);
276
+ if (!subcollection) {
277
+ throw new Error(`Subcollection '${subcollectionSlug}' not found in ${currentCollection.slug || currentCollection.dbPath}`);
278
+ }
279
+ currentCollection = subcollection;
280
+ collections.push(currentCollection);
281
+ }
282
+ }
283
+
284
+ return {
285
+ collections,
286
+ entityIds,
287
+ finalCollection: currentCollection
288
+ };
289
+ }
290
+
291
+ }
292
+
293
+ function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
294
+ // console.log("Comparing collection lists", a, b);
295
+ // return true;
296
+ if (a.length !== b.length) {
297
+ return false;
298
+ }
299
+ const aCopy = [...a];
300
+ const bCopy = [...b];
301
+ const aSorted = aCopy.sort((x, y) => x.slug.localeCompare(y.slug));
302
+ const bSorted = bCopy.sort((x, y) => x.slug.localeCompare(y.slug));
303
+ return aSorted.every((value, index) => areCollectionsEqual(value, bSorted[index]));
304
+ }
305
+
306
+ function areCollectionsEqual(a: EntityCollection, b: EntityCollection) {
307
+ const {
308
+ subcollections: subcollectionsA,
309
+ ...restA
310
+ } = a;
311
+ const {
312
+ subcollections: subcollectionsB,
313
+ ...restB
314
+ } = b;
315
+ if (!areCollectionListsEqual(subcollectionsA?.() ?? [], subcollectionsB?.() ?? [])) {
316
+ return false;
317
+ }
318
+ return deepEqual(removeFunctions(restA), removeFunctions(restB));
319
+ }
@@ -0,0 +1 @@
1
+ export * from "./CollectionRegistry";
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./util";
2
+ export * from "./collections";
@@ -0,0 +1,3 @@
1
+ export function toArray<T>(input?: T | T[]): T[] {
2
+ return Array.isArray(input) ? input : (input ? [input] : []);
3
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ AdditionalFieldDelegate,
3
+ ArrayProperty,
4
+ BooleanProperty,
5
+ DateProperty,
6
+ EntityCallbacks,
7
+ EntityCollection,
8
+ EnumValueConfig,
9
+ EnumValues,
10
+ GeopointProperty,
11
+ MapProperty,
12
+ NumberProperty, Properties,
13
+ Property,
14
+ PropertyConfig,
15
+ ReferenceProperty,
16
+ StringProperty,
17
+ User
18
+ } from "@rebasepro/types";
19
+
20
+ /**
21
+ * Identity function we use to defeat the type system of Typescript and build
22
+ * collection views with all its properties
23
+ * @param collection
24
+ * @group Builder
25
+ */
26
+ export function buildCollection<
27
+ M extends Record<string, any> = any,
28
+ USER extends User = User>
29
+ (
30
+ collection: EntityCollection<M, USER>
31
+ ): EntityCollection<M, USER> {
32
+ return collection;
33
+ }
34
+
35
+ /**
36
+ * Identity function we use to defeat the type system of Typescript and preserve
37
+ * the property keys.
38
+ * @param property
39
+ * @group Builder
40
+ */
41
+ export function buildProperty<T, P extends Property = Property>(
42
+ property: P
43
+ ):
44
+ P extends StringProperty ? StringProperty :
45
+ P extends NumberProperty ? NumberProperty :
46
+ P extends BooleanProperty ? BooleanProperty :
47
+ P extends DateProperty ? DateProperty :
48
+ P extends GeopointProperty ? GeopointProperty :
49
+ P extends ReferenceProperty ? ReferenceProperty :
50
+ P extends ArrayProperty ? ArrayProperty :
51
+ P extends MapProperty ? MapProperty : never {
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ return property as any;
54
+ }
55
+
56
+ /**
57
+ * Identity function we use to defeat the type system of Typescript and preserve
58
+ * the properties keys.
59
+ * @param properties
60
+ * @group Builder
61
+ */
62
+ export function buildProperties<M extends Record<string, any>>(
63
+ properties: Properties
64
+ ): Properties {
65
+ return properties;
66
+ }
67
+
68
+ /**
69
+ * Identity function we use to defeat the type system of Typescript and preserve
70
+ * the properties keys.
71
+ * @param propertiesOrBuilder
72
+ * @group Builder
73
+ */
74
+ export function buildPropertiesOrBuilder<M extends Record<string, any>>(
75
+ propertiesOrBuilder: Properties
76
+ ): Properties {
77
+ return propertiesOrBuilder;
78
+ }
79
+
80
+ /**
81
+ * Identity function we use to defeat the type system of Typescript and preserve
82
+ * the properties keys.
83
+ * @param enumValues
84
+ * @group Builder
85
+ */
86
+ export function buildEnum(
87
+ enumValues: EnumValues
88
+ ): EnumValues {
89
+ return enumValues;
90
+ }
91
+
92
+ /**
93
+ * Identity function we use to defeat the type system of Typescript and preserve
94
+ * the properties keys.
95
+ * @param enumValueConfig
96
+ * @group Builder
97
+ */
98
+ export function buildEnumValueConfig(
99
+ enumValueConfig: EnumValueConfig
100
+ ): EnumValueConfig {
101
+ return enumValueConfig;
102
+ }
103
+
104
+ /**
105
+ * Identity function we use to defeat the type system of Typescript and preserve
106
+ * the properties keys.
107
+ * @param callbacks
108
+ * @group Builder
109
+ */
110
+ export function buildEntityCallbacks<M extends Record<string, any> = any>(
111
+ callbacks: EntityCallbacks<M>
112
+ ): EntityCallbacks<M> {
113
+ return callbacks;
114
+ }
115
+
116
+ /**
117
+ * Identity function we use to defeat the type system of Typescript and build
118
+ * additional field delegates views with all its properties
119
+ * @param additionalFieldDelegate
120
+ * @group Builder
121
+ */
122
+ export function buildAdditionalFieldDelegate<M extends Record<string, any>, USER extends User = User>(
123
+ additionalFieldDelegate: AdditionalFieldDelegate<M, USER>
124
+ ): AdditionalFieldDelegate<M, USER> {
125
+ return additionalFieldDelegate;
126
+ }
127
+
128
+ /**
129
+ * Identity function we use to defeat the type system of Typescript and build
130
+ * additional field delegates views with all its properties
131
+ * @param propertyConfig
132
+ * @group Builder
133
+ */
134
+ export function buildFieldConfig(
135
+ propertyConfig: PropertyConfig
136
+ ): PropertyConfig {
137
+ return propertyConfig;
138
+ }
@@ -0,0 +1,115 @@
1
+ import { EntityCallbacks, Properties } from "@rebasepro/types";
2
+ import { mergeDeep } from "./objects";
3
+
4
+ /**
5
+ * Helper function to recursively check if there are any callbacks in the properties.
6
+ */
7
+ function hasPropertyCallbacks(properties: Properties, callbackName: "afterRead" | "beforeSave"): boolean {
8
+ if (!properties) return false;
9
+ for (const property of Object.values(properties)) {
10
+ if (property.callbacks?.[callbackName]) return true;
11
+ if (property.type === "map" && property.properties) {
12
+ if (hasPropertyCallbacks(property.properties, callbackName)) return true;
13
+ } else if (property.type === "array" && property.of) {
14
+ const ofs = Array.isArray(property.of) ? property.of : [property.of];
15
+ for (const of of ofs) {
16
+ if (of.callbacks?.[callbackName]) return true;
17
+ if (of.type === "map" && of.properties && hasPropertyCallbacks(of.properties, callbackName)) return true;
18
+ }
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+
24
+ /**
25
+ * Recursively process properties to apply field-level hooks.
26
+ */
27
+ async function processProperties(
28
+ properties: Properties,
29
+ values: Record<string, unknown>,
30
+ previousValues: Record<string, unknown>,
31
+ propsContext: unknown,
32
+ callbackName: "afterRead" | "beforeSave"
33
+ ): Promise<Record<string, unknown>> {
34
+ if (!values || typeof values !== "object") return values;
35
+
36
+ let result = { ...values };
37
+
38
+ for (const [key, property] of Object.entries(properties)) {
39
+ if (result[key] === undefined) continue;
40
+
41
+ let currentValue = result[key];
42
+ let previousValue = previousValues?.[key];
43
+
44
+ // 1. Array Property
45
+ if (property.type === "array" && Array.isArray(currentValue)) {
46
+ // We only support traversing single-type arrays for hooks currently to avoid complex union matching
47
+ if (property.of && !Array.isArray(property.of)) {
48
+ currentValue = await Promise.all(currentValue.map(async (item, index) => {
49
+ const prevItem = Array.isArray(previousValue) ? previousValue[index] : undefined;
50
+ // Mock a properties object to process a single item
51
+ const singlePropData = { "_tmp": property.of } as Properties;
52
+ const res = await processProperties(singlePropData, { "_tmp": item }, { "_tmp": prevItem }, propsContext, callbackName);
53
+ return res["_tmp"];
54
+ }));
55
+ }
56
+ }
57
+ // 2. Map Property
58
+ else if (property.type === "map" && property.properties && typeof currentValue === "object") {
59
+ currentValue = await processProperties(property.properties, currentValue as Record<string, unknown>, (previousValue ?? {}) as Record<string, unknown>, propsContext, callbackName);
60
+ }
61
+
62
+ // 3. Property's own callback
63
+ if (property.callbacks?.[callbackName]) {
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ const cbRes = await Promise.resolve(property.callbacks[callbackName]({
66
+ ...(propsContext as Record<string, unknown>),
67
+ value: currentValue,
68
+ previousValue
69
+ } as any));
70
+ if (cbRes !== undefined) {
71
+ currentValue = cbRes;
72
+ }
73
+ }
74
+
75
+ result[key] = currentValue;
76
+ }
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Helper function to extract field-level PropertyCallbacks from a properties schema
82
+ * and wrap them into an EntityCallbacks object recursively.
83
+ */
84
+ export const buildPropertyCallbacks = (properties: Properties): EntityCallbacks | undefined => {
85
+ if (!properties) return undefined;
86
+
87
+ const propertyCallbacks: EntityCallbacks = {};
88
+
89
+ if (hasPropertyCallbacks(properties, "afterRead")) {
90
+ propertyCallbacks.afterRead = async (props) => {
91
+ const processedValues = await processProperties(
92
+ properties,
93
+ props.entity.values as Record<string, unknown>,
94
+ props.entity.values as Record<string, unknown>,
95
+ props as unknown,
96
+ "afterRead"
97
+ );
98
+ return { ...props.entity, values: processedValues };
99
+ };
100
+ }
101
+
102
+ if (hasPropertyCallbacks(properties, "beforeSave")) {
103
+ propertyCallbacks.beforeSave = async (props) => {
104
+ return await processProperties(
105
+ properties,
106
+ props.values as Record<string, unknown>,
107
+ (props.previousValues ?? {}) as Record<string, unknown>,
108
+ props as unknown,
109
+ "beforeSave"
110
+ );
111
+ };
112
+ }
113
+
114
+ return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : undefined;
115
+ };