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