@webda/models 0.0.0-stage → 4.0.0-beta.4

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 (51) hide show
  1. package/LICENSE +166 -0
  2. package/README.md +146 -2
  3. package/lib/index.d.ts +11 -0
  4. package/lib/index.d.ts.map +1 -0
  5. package/lib/index.js +11 -0
  6. package/lib/index.js.map +1 -0
  7. package/lib/mock.d.ts +72 -0
  8. package/lib/mock.d.ts.map +1 -0
  9. package/lib/mock.js +83 -0
  10. package/lib/mock.js.map +1 -0
  11. package/lib/model.model.d.ts +286 -0
  12. package/lib/model.model.d.ts.map +1 -0
  13. package/lib/model.model.js +336 -0
  14. package/lib/model.model.js.map +1 -0
  15. package/lib/relations.d.ts +705 -0
  16. package/lib/relations.d.ts.map +1 -0
  17. package/lib/relations.js +938 -0
  18. package/lib/relations.js.map +1 -0
  19. package/lib/repositories/abstract.d.ts +138 -0
  20. package/lib/repositories/abstract.d.ts.map +1 -0
  21. package/lib/repositories/abstract.js +280 -0
  22. package/lib/repositories/abstract.js.map +1 -0
  23. package/lib/repositories/conformance.d.ts +40 -0
  24. package/lib/repositories/conformance.d.ts.map +1 -0
  25. package/lib/repositories/conformance.js +38 -0
  26. package/lib/repositories/conformance.js.map +1 -0
  27. package/lib/repositories/event.d.ts +101 -0
  28. package/lib/repositories/event.d.ts.map +1 -0
  29. package/lib/repositories/event.js +228 -0
  30. package/lib/repositories/event.js.map +1 -0
  31. package/lib/repositories/hooks.d.ts +88 -0
  32. package/lib/repositories/hooks.d.ts.map +1 -0
  33. package/lib/repositories/hooks.js +105 -0
  34. package/lib/repositories/hooks.js.map +1 -0
  35. package/lib/repositories/memory.d.ts +262 -0
  36. package/lib/repositories/memory.d.ts.map +1 -0
  37. package/lib/repositories/memory.js +574 -0
  38. package/lib/repositories/memory.js.map +1 -0
  39. package/lib/repositories/repository.d.ts +238 -0
  40. package/lib/repositories/repository.d.ts.map +1 -0
  41. package/lib/repositories/repository.js +19 -0
  42. package/lib/repositories/repository.js.map +1 -0
  43. package/lib/storable.d.ts +210 -0
  44. package/lib/storable.d.ts.map +1 -0
  45. package/lib/storable.js +76 -0
  46. package/lib/storable.js.map +1 -0
  47. package/lib/types.d.ts +230 -0
  48. package/lib/types.d.ts.map +1 -0
  49. package/lib/types.js +12 -0
  50. package/lib/types.js.map +1 -0
  51. package/package.json +60 -3
@@ -0,0 +1,938 @@
1
+ var _a, _b, _c, _d;
2
+ import { isStorable, PrimaryKeyEquals, WEBDA_PRIMARY_KEY } from "./storable.js";
3
+ import { useRepository } from "./repositories/hooks.js";
4
+ import * as WebdaQL from "@webda/ql";
5
+ /** Symbol key holding the parent Storable of a relation. */
6
+ //export const RelationParent = Symbol("RelationParent");
7
+ /** Symbol key holding the primary key value of a relation target. */
8
+ export const RelationKey = Symbol("RelationKey");
9
+ /** Symbol key holding the repository used to resolve a relation. */
10
+ export const RelationRepository = Symbol("RelationRepository");
11
+ /** Symbol key identifying the role of a relation (Link, Related, Links, CustomLinks). */
12
+ export const RelationRole = Symbol("RelationRole");
13
+ /** Symbol key holding custom data on a ModelRefCustom. */
14
+ export const RelationData = Symbol("RelationData");
15
+ /** Symbol key holding the model. */
16
+ export const RelationModel = Symbol("RelationModel");
17
+ /** Symbol key holding the type of custom attributes on a ModelLinksArray. */
18
+ export const RelationAttributes = Symbol("RelationAttributes");
19
+ /**
20
+ * Assign non symbol properties from source to target
21
+ * @param target - the destination object
22
+ * @param source - the source object
23
+ */
24
+ export function assignNonSymbols(target, source) {
25
+ Object.keys(source).forEach(key => {
26
+ target[key] = source[key];
27
+ });
28
+ }
29
+ /**
30
+ * A reference to a model
31
+ */
32
+ export class ModelRef {
33
+ /**
34
+ * Create a new model reference
35
+ * @param key - Primary key of the referenced model
36
+ * @param repository - Repository to resolve the reference against
37
+ * @param parent - Parent storable that owns this reference
38
+ */
39
+ constructor(key, repository, parent) {
40
+ // this[RelationParent] = parent;
41
+ this[RelationKey] = key;
42
+ this[RelationRepository] = repository;
43
+ }
44
+ /**
45
+ * Get the repository and ensure it's initialized
46
+ * @returns the repository instance
47
+ */
48
+ getRepository() {
49
+ if (!this[RelationRepository]) {
50
+ throw new Error("Relation repository is not initialized");
51
+ }
52
+ return this[RelationRepository];
53
+ }
54
+ /**
55
+ * Get the primary key and ensure it's initialized
56
+ * @returns the primary key value
57
+ */
58
+ getKey() {
59
+ return this.getPrimaryKey();
60
+ }
61
+ /**
62
+ * Get the model
63
+ * @returns the primary key value
64
+ */
65
+ getPrimaryKey() {
66
+ if (!this[RelationKey]) {
67
+ throw new Error("Relation key is not initialized");
68
+ }
69
+ return this[RelationKey];
70
+ }
71
+ /**
72
+ * Serialize the reference to its primary key value
73
+ * @returns the primary key value
74
+ */
75
+ toJSON() {
76
+ return this.getKey();
77
+ }
78
+ /**
79
+ * Set the attribute of the model
80
+ * @param attribute - the attribute path to set
81
+ * @param value - the new attribute value
82
+ * @returns a promise resolving when complete
83
+ */
84
+ setAttribute(attribute, value) {
85
+ return this.getRepository().setAttribute(this.getKey(), attribute, value);
86
+ }
87
+ /**
88
+ * Patch the model
89
+ * @param data - the partial data to patch
90
+ * @param conditionField - optional field for optimistic locking
91
+ * @param condition - expected value for the condition field
92
+ * @returns a promise resolving when complete
93
+ */
94
+ patch(data, conditionField, condition) {
95
+ return this.getRepository().patch(this.getKey(), data, conditionField, condition);
96
+ }
97
+ /**
98
+ * Fully replace the model data (except primary key fields)
99
+ * @param data - Complete model data excluding primary key
100
+ * @param conditionField - Optional field for optimistic locking
101
+ * @param condition - Expected value for the condition field
102
+ * @returns a promise resolving when complete
103
+ */
104
+ update(data, conditionField, condition) {
105
+ return this.getRepository().update({ ...data, ...this.getKey() }, conditionField, condition);
106
+ }
107
+ /**
108
+ * Get the model referenced by the key
109
+ * @returns the resolved model instance
110
+ */
111
+ get() {
112
+ return this.getRepository().get(this.getKey());
113
+ }
114
+ /**
115
+ * Delete the model
116
+ *
117
+ * If a condition is specified, it will be used to check if the model can be deleted
118
+ *
119
+ * @param conditionField - optional field for optimistic locking
120
+ * @param condition - expected value for the condition field
121
+ * @returns a promise resolving when complete
122
+ */
123
+ delete(conditionField, condition) {
124
+ return this.getRepository().delete(this.getKey(), conditionField, condition);
125
+ }
126
+ /**
127
+ * Check if the model exists in the repository
128
+ * @returns true if the model exists
129
+ */
130
+ exists() {
131
+ return this.getRepository().exists(this.getKey());
132
+ }
133
+ /**
134
+ * Increment the attributes of the model
135
+ * @param info The attributes to increment
136
+ * @param conditionField - optional field for optimistic locking
137
+ * @param condition - expected value for the condition field
138
+ * @returns a promise resolving when complete
139
+ */
140
+ incrementAttributes(info, conditionField, condition) {
141
+ return this.getRepository().incrementAttributes(this.getKey(), info, conditionField, condition);
142
+ }
143
+ /**
144
+ * Add an item to a collection
145
+ * @param collection - the collection field name
146
+ * @param item - the item to add or update
147
+ * @param index - optional position to insert at
148
+ * @param itemWriteConditionField - optional field for item write condition
149
+ * @param itemWriteCondition - expected value for the item write condition
150
+ * @returns a promise resolving when complete
151
+ */
152
+ upsertItemToCollection(collection, item, index, itemWriteConditionField, itemWriteCondition) {
153
+ return this.getRepository().upsertItemToCollection(this.getKey(), collection, item, index, itemWriteConditionField, itemWriteCondition);
154
+ }
155
+ /**
156
+ * Remove an item from a collection
157
+ * @param collection - the collection field name
158
+ * @param index - the position to remove from
159
+ * @param itemWriteConditionField - optional field for item write condition
160
+ * @param itemWriteCondition - expected value for the item write condition
161
+ * @returns a promise resolving when complete
162
+ */
163
+ deleteItemFromCollection(collection, index, itemWriteConditionField, itemWriteCondition) {
164
+ return this.getRepository().deleteItemFromCollection(this.getKey(), collection, index, itemWriteConditionField, itemWriteCondition);
165
+ }
166
+ /**
167
+ * Remove an attribute from the model
168
+ * @param attribute - the attribute to remove
169
+ * @param conditionField - optional field for optimistic locking
170
+ * @param condition - expected value for the condition field
171
+ * @returns a promise resolving when complete
172
+ */
173
+ removeAttribute(attribute, conditionField, condition) {
174
+ return this.getRepository().removeAttribute(this.getKey(), attribute, conditionField, condition);
175
+ }
176
+ /**
177
+ * Increment only one attribute of the model
178
+ * @param property - the numeric property to increment
179
+ * @param value - the increment amount
180
+ * @param conditionField - optional field for optimistic locking
181
+ * @param condition - expected value for the condition field
182
+ * @returns a promise resolving when complete
183
+ */
184
+ incrementAttribute(property, value, conditionField, condition) {
185
+ return this.incrementAttributes([{ property: property, value: value }], conditionField, condition);
186
+ }
187
+ }
188
+ /**
189
+ * Model reference with create and upsert methods
190
+ */
191
+ export class ModelRefWithCreate extends ModelRef {
192
+ /**
193
+ * Upsert a model reference
194
+ * @param data - the model data excluding primary key
195
+ * @returns the upserted model instance
196
+ */
197
+ upsert(data) {
198
+ return this.getRepository().upsert({
199
+ ...data,
200
+ ...this.getRepository().getPrimaryKey(this.getKey(), true)
201
+ });
202
+ }
203
+ /**
204
+ * Create a model reference
205
+ *
206
+ * It will fail if the model already exists
207
+ * @param data - the model data excluding primary key
208
+ * @returns the created model instance
209
+ */
210
+ create(data) {
211
+ return this.getRepository().create({
212
+ ...data,
213
+ ...this.getRepository().getPrimaryKey(this.getKey(), true)
214
+ });
215
+ }
216
+ }
217
+ /**
218
+ * Define a 1:n relation on the current model to another model
219
+ *
220
+ * T is the model to link to
221
+ * _K is the attribute in T to query
222
+ *
223
+ * _K is not used but is required to complete the graph
224
+ * _K define the attribute to use to load the related objects
225
+ *
226
+ * In SQL if current model is TableA and T is TableB, _K is the foreign key in TableB
227
+ *
228
+ * TODO Deduce attribute from the ModelParent on the other side when "" is used
229
+ */
230
+ export class ModelRelated {
231
+ /** Create a new ModelRelated
232
+ * @param targetClass - the target model class
233
+ * @param object - the source object owning the relation
234
+ * @param attribute - the attribute name linking the relation
235
+ */
236
+ constructor(targetClass, object, attribute = "") {
237
+ this.object = object;
238
+ this.attribute = attribute;
239
+ this.repoSource = useRepository(targetClass);
240
+ }
241
+ /**
242
+ * Build a WebdaQL query string that filters related objects by this model's primary key.
243
+ * @param query - optional additional query filter
244
+ * @returns the constructed query string
245
+ */
246
+ getQuery(query = "") {
247
+ // escape() makes keys containing quotes or backslashes round-trip through the parser
248
+ return WebdaQL.PrependCondition(query, WebdaQL.escape([`${this.attribute} = `, ""], [String(this.object.getPrimaryKey())]));
249
+ }
250
+ /**
251
+ * Query the related objects
252
+ * @param query - optional additional query filter, optionally with `?` or `:name` placeholders
253
+ * @param params - values for the placeholders: an array for `?`, an object for `:name`
254
+ * @returns the query results with optional continuation token
255
+ */
256
+ async query(query = "", params) {
257
+ // Bind before merging the relation condition, which parses the query
258
+ const q = this.getQuery(params === undefined ? query : WebdaQL.bind(query, params));
259
+ return this.repoSource.query(q);
260
+ }
261
+ /**
262
+ * Iterate through linked objects
263
+ * @param query - the query filter, optionally with `?` or `:name` placeholders
264
+ * @param params - values for the placeholders: an array for `?`, an object for `:name`
265
+ * @returns an async iterable of linked objects
266
+ */
267
+ iterate(query, params) {
268
+ const q = this.getQuery(params === undefined ? query : WebdaQL.bind(query, params));
269
+ return this.repoSource.iterate(q);
270
+ }
271
+ /**
272
+ * It is a helper and should not be serialized
273
+ */
274
+ toJSON() {
275
+ return;
276
+ }
277
+ }
278
+ /**
279
+ * Define a link to n:1 or 1:1 relation on the current model to another model
280
+ *
281
+ * T is the model to link to
282
+ *
283
+ * This is expected to be the symetric of ModelRelated or ModelLink in a 1:1 relation
284
+ *
285
+ * ```
286
+ * class Album extends Model {
287
+ * artist: ModelLink<Artist>;
288
+ * }
289
+ * ```
290
+ */
291
+ export class ModelLink {
292
+ static { _a = RelationRole; }
293
+ /** Create a new ModelLink
294
+ * @param model - the model class to link to
295
+ */
296
+ constructor(
297
+ /**
298
+ * The repository of the object to link
299
+ */
300
+ model) {
301
+ this[_a] = "Link";
302
+ this[RelationModel] = model;
303
+ }
304
+ /**
305
+ * Resolve the link and fetch the target model from the repository
306
+ * @returns the linked model instance
307
+ */
308
+ async get() {
309
+ if (!this[RelationKey]) {
310
+ throw new Error("Relation key is not initialized");
311
+ }
312
+ return (await useRepository(this[RelationModel]).get(this[RelationKey]));
313
+ }
314
+ /**
315
+ * Update the link to point to a different target
316
+ * @param id - New target: a primary key, model instance, or string UID
317
+ * @returns this link instance
318
+ * @WebdaAutoSetter
319
+ */
320
+ set(id) {
321
+ this[RelationKey] = isStorable(id)
322
+ ? id.getPrimaryKey()
323
+ : typeof id === "string"
324
+ ? useRepository(this[RelationModel]).parseUID(id)
325
+ : id;
326
+ return this;
327
+ }
328
+ /**
329
+ * Return the string representation of the linked key
330
+ * @returns the string representation
331
+ */
332
+ toString() {
333
+ if (!this[RelationKey]) {
334
+ throw new Error("Relation key is not initialized");
335
+ }
336
+ return this[RelationKey].toString();
337
+ }
338
+ /**
339
+ * Serialize the link to its primary key value
340
+ * @returns the primary key value
341
+ */
342
+ toJSON() {
343
+ if (!this[RelationKey]) {
344
+ throw new Error("Relation key is not initialized");
345
+ }
346
+ return this[RelationKey];
347
+ }
348
+ /**
349
+ * Get the primary key of the linked model
350
+ * @returns the primary key value
351
+ */
352
+ getPrimaryKey() {
353
+ if (!this[RelationKey]) {
354
+ throw new Error("Relation key is not initialized");
355
+ }
356
+ return this[RelationKey];
357
+ }
358
+ }
359
+ /**
360
+ * Link to a collection of objects
361
+ *
362
+ * Uses composition instead of extending Array for better maintainability,
363
+ * performance, and type safety.
364
+ *
365
+ * This allows to do a n:m or 1:n relation between two models
366
+ *
367
+ * It does not require to have a symetric relation on the other side
368
+ * The array contains uuid of the linked objects
369
+ */
370
+ export class ModelLinksSimpleArray {
371
+ /** Create a new ModelLinksSimpleArray
372
+ * @param model - the model class for linked items
373
+ * @param content - initial array of primary keys
374
+ * @param parentObject - optional parent object owning this relation
375
+ */
376
+ constructor(model, content = [], parentObject) {
377
+ this[_b] = "Links";
378
+ // COMPOSITION: Internal array instead of extending Array
379
+ this.items = [];
380
+ this[RelationRepository] = useRepository(model);
381
+ // Initialize internal array
382
+ this.items = content.map(c => this.getModelRef(c));
383
+ // Return a Proxy to support indexed access like an array
384
+ return new Proxy(this, {
385
+ get(target, prop) {
386
+ // Handle numeric indices
387
+ if (typeof prop === "string" && /^\d+$/.test(prop)) {
388
+ return target.items[parseInt(prop, 10)];
389
+ }
390
+ // Handle all other properties normally
391
+ return target[prop];
392
+ },
393
+ set(target, prop, value) {
394
+ // Handle numeric indices
395
+ if (typeof prop === "string" && /^\d+$/.test(prop)) {
396
+ target.items[parseInt(prop, 10)] = value;
397
+ return true;
398
+ }
399
+ // Handle all other properties normally
400
+ target[prop] = value;
401
+ return true;
402
+ }
403
+ });
404
+ }
405
+ /**
406
+ * Add items to the end of the collection
407
+ * @param items - items to add
408
+ * @returns the new collection length
409
+ */
410
+ push(...items) {
411
+ const result = this.items.push(...items.map(i => this.getModelRef(i)));
412
+ this.setDirty();
413
+ return result;
414
+ }
415
+ /**
416
+ * Add items to the beginning of the collection
417
+ * @param items - items to prepend
418
+ * @returns the new collection length
419
+ */
420
+ unshift(...items) {
421
+ const result = this.items.unshift(...items.map(i => this.getModelRef(i)));
422
+ this.setDirty();
423
+ return result;
424
+ }
425
+ /**
426
+ * Remove and return the last item
427
+ * @returns the removed item, or undefined
428
+ */
429
+ pop() {
430
+ const result = this.items.pop();
431
+ if (result !== undefined) {
432
+ this.setDirty();
433
+ }
434
+ return result;
435
+ }
436
+ /**
437
+ * Remove and return the first item
438
+ * @returns the removed item, or undefined
439
+ */
440
+ shift() {
441
+ const result = this.items.shift();
442
+ if (result !== undefined) {
443
+ this.setDirty();
444
+ }
445
+ return result;
446
+ }
447
+ /**
448
+ * Remove/add items at specified index
449
+ * @param start - the start index
450
+ * @param deleteCount - number of items to remove
451
+ * @param rest - items to insert
452
+ * @returns the removed items
453
+ */
454
+ splice(start, deleteCount, ...rest) {
455
+ const result = this.items.splice(start, deleteCount, ...rest.map(i => this.getModelRef(i)));
456
+ if (result.length > 0 || rest.length > 0) {
457
+ this.setDirty();
458
+ }
459
+ return result;
460
+ }
461
+ /**
462
+ * Add a model to the collection
463
+ * @param model - the model to add
464
+ * @deprecated use push instead
465
+ */
466
+ add(model) {
467
+ this.push(model);
468
+ }
469
+ /**
470
+ * Set the collection to a new set of items
471
+ * @param items - the new items to set
472
+ * @WebdaAutoSetter
473
+ */
474
+ set(items) {
475
+ this.items = items?.map(i => this.getModelRef(i)) || [];
476
+ this.setDirty();
477
+ }
478
+ /**
479
+ * Remove item by reference or primary key
480
+ * @param model - the item to remove
481
+ * @returns true if the item was found and removed
482
+ */
483
+ remove(model) {
484
+ const uuid = this.getModelRef(model).getPrimaryKey().toString();
485
+ const index = this.items.findIndex(m => m.getPrimaryKey().toString() === uuid);
486
+ if (index >= 0) {
487
+ this.items.splice(index, 1);
488
+ this.setDirty();
489
+ return true;
490
+ }
491
+ return false;
492
+ }
493
+ /**
494
+ * Get the number of items
495
+ * @returns the item count
496
+ */
497
+ get length() {
498
+ return this.items.length;
499
+ }
500
+ /**
501
+ * Get item at index
502
+ * @param index - the array index
503
+ * @returns the item, or undefined
504
+ */
505
+ at(index) {
506
+ return this.items[index];
507
+ }
508
+ /**
509
+ * Find item by predicate
510
+ * @param predicate - the search predicate
511
+ * @returns the found item, or undefined
512
+ */
513
+ find(predicate) {
514
+ return this.items.find(predicate);
515
+ }
516
+ /**
517
+ * Find index by predicate
518
+ * @param predicate - the search predicate
519
+ * @returns the index, or -1
520
+ */
521
+ findIndex(predicate) {
522
+ return this.items.findIndex(predicate);
523
+ }
524
+ /**
525
+ * Map over items (does NOT mutate)
526
+ * @param callback - the mapping function
527
+ * @returns the mapped array
528
+ */
529
+ map(callback) {
530
+ return this.items.map(callback);
531
+ }
532
+ /**
533
+ * Filter items (does NOT mutate)
534
+ * @param predicate - the filter predicate
535
+ * @returns the filtered items
536
+ */
537
+ filter(predicate) {
538
+ return this.items.filter(predicate);
539
+ }
540
+ /**
541
+ * Execute callback for each item
542
+ * @param callback - the function to execute for each item
543
+ */
544
+ forEach(callback) {
545
+ this.items.forEach(callback);
546
+ }
547
+ /**
548
+ * Check if any item matches predicate
549
+ * @param predicate - the test predicate
550
+ * @returns true if any item matches
551
+ */
552
+ some(predicate) {
553
+ return this.items.some(predicate);
554
+ }
555
+ /**
556
+ * Check if all items match predicate
557
+ * @param predicate - the test predicate
558
+ * @returns true if all items match
559
+ */
560
+ every(predicate) {
561
+ return this.items.every(predicate);
562
+ }
563
+ /**
564
+ * Convert to JSON
565
+ * @returns array of primary key values
566
+ */
567
+ toJSON() {
568
+ return this.items.map(m => m.getPrimaryKey());
569
+ }
570
+ /**
571
+ * Make the collection iterable (for...of loops)
572
+ * @returns an iterator over the items
573
+ */
574
+ [(_b = RelationRole, Symbol.iterator)]() {
575
+ return this.items[Symbol.iterator]();
576
+ }
577
+ /**
578
+ * Convert any accepted input (string, key, ref, or model) into a ModelRef
579
+ * @param model - the input to convert
580
+ * @returns the model reference
581
+ */
582
+ getModelRef(model) {
583
+ let modelRef;
584
+ if (typeof model === "string") {
585
+ modelRef = new ModelRef(this[RelationRepository].parseUID(model), this[RelationRepository]);
586
+ }
587
+ else if (model instanceof ModelRef) {
588
+ modelRef = model;
589
+ }
590
+ else if (isStorable(model)) {
591
+ modelRef = new ModelRef(model.getPrimaryKey(), this[RelationRepository]);
592
+ }
593
+ else {
594
+ modelRef = new ModelRef(model, this[RelationRepository]);
595
+ }
596
+ return modelRef;
597
+ }
598
+ /**
599
+ * Mark the parent model's property for this collection as dirty
600
+ */
601
+ setDirty() {
602
+ // no-op: dirty tracking is handled by the deep proxy in track()
603
+ }
604
+ }
605
+ /**
606
+ * ModelRef with custom properties
607
+ *
608
+ * This is used to define a link to a collection of objects with additional data
609
+ *
610
+ * It does not require to have a symetric relation on the other side
611
+ * The array contains uuid of the linked objects and additional properties defined as K
612
+ *
613
+ * Sample usage:
614
+ * ```typescript
615
+ * class MyModel extends AbstractCoreModel {
616
+ * _links: ModelLinksArray<OtherModel, { custom: string }>;
617
+ * }
618
+ * ```
619
+ */
620
+ class ModelRefCustom extends ModelRef {
621
+ /** Create a new ModelRefCustom
622
+ * @param key - the primary key of the referenced object
623
+ * @param repo - the repository managing the referenced model
624
+ * @param data - custom data associated with this reference
625
+ * @param parent - optional parent storable
626
+ */
627
+ constructor(key, repo, data, parent) {
628
+ super(key, repo, parent);
629
+ this[RelationData] = data;
630
+ assignNonSymbols(this, data);
631
+ // We might want to explore moving data in sub-classes
632
+ }
633
+ /**
634
+ * Override toJSON to include custom properties along with primary key
635
+ * @returns the relation data
636
+ */
637
+ toJSON() {
638
+ // Return the relation data which should already include the primary key
639
+ // along with any additional custom properties
640
+ return this[RelationData];
641
+ }
642
+ }
643
+ /**
644
+ * Link to a collection of objects including some additional data
645
+ *
646
+ * Uses composition instead of extending Array for better maintainability,
647
+ * performance, and type safety. Provides array-like interface through
648
+ * explicit methods and iterators.
649
+ *
650
+ * It does not require to have a symetric relation on the other side
651
+ * The array contains uuid of the linked objects and additional properties defined as K
652
+ *
653
+ * Sample usage:
654
+ * ```typescript
655
+ * class MyModel extends AbstractCoreModel {
656
+ * _links: ModelLinksArray<OtherModel, { custom: string }>;
657
+ * }
658
+ * ```
659
+ */
660
+ export class ModelLinksArray {
661
+ /** Create a new ModelLinksCustomArray
662
+ * @param repo - the repository for the linked model
663
+ * @param content - initial array of primary keys with custom data
664
+ */
665
+ constructor(repo, content = []
666
+ //parentObject?: T
667
+ ) {
668
+ this.repo = repo;
669
+ this[_c] = "CustomLinks";
670
+ // COMPOSITION: Internal array instead of extending Array
671
+ this.items = [];
672
+ //this[RelationParent] = parentObject!;
673
+ // Initialize internal array
674
+ this.items = content.map(c => new ModelRefCustom(repo.getPrimaryKey(c), repo, c));
675
+ // Return a Proxy to support indexed access like an array
676
+ return new Proxy(this, {
677
+ get(target, prop) {
678
+ // Handle numeric indices
679
+ if (typeof prop === "string" && /^\d+$/.test(prop)) {
680
+ return target.items[parseInt(prop, 10)];
681
+ }
682
+ // Handle all other properties normally
683
+ return target[prop];
684
+ },
685
+ set(target, prop, value) {
686
+ // Handle numeric indices
687
+ if (typeof prop === "string" && /^\d+$/.test(prop)) {
688
+ target.items[parseInt(prop, 10)] = value;
689
+ return true;
690
+ }
691
+ // Handle all other properties normally
692
+ target[prop] = value;
693
+ return true;
694
+ }
695
+ });
696
+ }
697
+ /**
698
+ * Add a model to the collection
699
+ * @param model - the model to add
700
+ * @deprecated use push instead
701
+ */
702
+ add(model) {
703
+ this.push(new ModelRefCustom(this.repo.getPrimaryKey(model), this.repo, model));
704
+ }
705
+ /**
706
+ * Add items to the end of the collection
707
+ * @param items - items to add
708
+ * @returns the new collection length
709
+ */
710
+ push(...items) {
711
+ const result = this.items.push(...items.map(i => this.getModelRef(i)));
712
+ this.setDirty();
713
+ return result;
714
+ }
715
+ /**
716
+ * Remove and return the last item
717
+ * @returns the removed item, or undefined
718
+ */
719
+ pop() {
720
+ const result = this.items.pop();
721
+ if (result !== undefined) {
722
+ this.setDirty();
723
+ }
724
+ return result;
725
+ }
726
+ /**
727
+ * Remove and return the first item
728
+ * @returns the removed item, or undefined
729
+ */
730
+ shift() {
731
+ const result = this.items.shift();
732
+ if (result !== undefined) {
733
+ this.setDirty();
734
+ }
735
+ return result;
736
+ }
737
+ /**
738
+ * Add items to the beginning of the collection
739
+ * @param items - items to prepend
740
+ * @returns the new collection length
741
+ */
742
+ unshift(...items) {
743
+ const result = this.items.unshift(...items.map(i => this.getModelRef(i)));
744
+ this.setDirty();
745
+ return result;
746
+ }
747
+ /**
748
+ * Remove/add items at specified index
749
+ * @param start - the start index
750
+ * @param deleteCount - number of items to remove
751
+ * @param rest - items to insert
752
+ * @returns the removed items
753
+ */
754
+ splice(start, deleteCount, ...rest) {
755
+ const result = this.items.splice(start, deleteCount, ...rest.map(i => this.getModelRef(i)));
756
+ if (result.length > 0 || rest.length > 0) {
757
+ this.setDirty();
758
+ }
759
+ return result;
760
+ }
761
+ /**
762
+ * Remove item by reference or primary key
763
+ * @param model - the item to remove
764
+ * @returns true if found and removed
765
+ */
766
+ remove(model) {
767
+ const uuid = typeof model["getPrimaryKey"] === "function" ? model["getPrimaryKey"]() : model;
768
+ const index = this.items.findIndex(m => PrimaryKeyEquals(m.getPrimaryKey(), uuid));
769
+ if (index >= 0) {
770
+ this.items.splice(index, 1);
771
+ this.setDirty();
772
+ return true;
773
+ }
774
+ return false;
775
+ }
776
+ /**
777
+ * Get the number of items
778
+ * @returns the item count
779
+ */
780
+ get length() {
781
+ return this.items.length;
782
+ }
783
+ /**
784
+ * Get item at index
785
+ * @param index - the array index
786
+ * @returns the item, or undefined
787
+ */
788
+ at(index) {
789
+ return this.items[index];
790
+ }
791
+ /**
792
+ * Find item by predicate
793
+ * @param predicate - the search predicate
794
+ * @returns the found item, or undefined
795
+ */
796
+ find(predicate) {
797
+ return this.items.find(predicate);
798
+ }
799
+ /**
800
+ * Find index by predicate
801
+ * @param predicate - the search predicate
802
+ * @returns the index, or -1
803
+ */
804
+ findIndex(predicate) {
805
+ return this.items.findIndex(predicate);
806
+ }
807
+ /**
808
+ * Check if item exists in collection
809
+ * @param search - the item to look for
810
+ * @param fromIndex - optional start index
811
+ * @returns true if found
812
+ */
813
+ includes(search, fromIndex) {
814
+ return this.items.includes(search, fromIndex);
815
+ }
816
+ /**
817
+ * Map over items (does NOT mutate)
818
+ * @param callback - the mapping function
819
+ * @returns the mapped array
820
+ */
821
+ map(callback) {
822
+ return this.items.map(callback);
823
+ }
824
+ /**
825
+ * Filter items (does NOT mutate)
826
+ * @param predicate - the filter predicate
827
+ * @returns the filtered items
828
+ */
829
+ filter(predicate) {
830
+ return this.items.filter(predicate);
831
+ }
832
+ /**
833
+ * Execute callback for each item
834
+ * @param callback - the function to execute for each item
835
+ */
836
+ forEach(callback) {
837
+ this.items.forEach(callback);
838
+ }
839
+ /**
840
+ * Check if any item matches predicate
841
+ * @param predicate - the test predicate
842
+ * @returns true if any item matches
843
+ */
844
+ some(predicate) {
845
+ return this.items.some(predicate);
846
+ }
847
+ /**
848
+ * Check if all items match predicate
849
+ * @param predicate - the test predicate
850
+ * @returns true if all items match
851
+ */
852
+ every(predicate) {
853
+ return this.items.every(predicate);
854
+ }
855
+ /**
856
+ * Convert to JSON
857
+ * @returns array of serialized items
858
+ */
859
+ toJSON() {
860
+ return this.items;
861
+ }
862
+ /**
863
+ * Make the collection iterable (for...of loops)
864
+ * @returns an iterator over the items
865
+ */
866
+ [(_c = RelationRole, Symbol.iterator)]() {
867
+ return this.items[Symbol.iterator]();
868
+ }
869
+ /**
870
+ * Convert input into a ModelRefCustom, preserving custom properties
871
+ * @param model - the input to convert
872
+ * @returns the model reference with custom properties
873
+ */
874
+ getModelRef(model) {
875
+ return ((model instanceof ModelRefCustom
876
+ ? model
877
+ : new ModelRefCustom(this.repo.getPrimaryKey(model), this.repo, model)));
878
+ }
879
+ /**
880
+ * Mark the parent model's property for this collection as dirty
881
+ */
882
+ setDirty() {
883
+ // no-op: dirty tracking is handled by the deep proxy in track()
884
+ }
885
+ }
886
+ /**
887
+ * Define a ModelRef with custom properties for map
888
+ * Exclude the PrimaryKey from the JSON
889
+ *
890
+ * @deprecated
891
+ */
892
+ export class ModelRefCustomMap extends ModelRefCustom {
893
+ /**
894
+ * Serialize the relation data as a plain object (excluding the primary key).
895
+ * @returns the relation data
896
+ */
897
+ toJSON() {
898
+ return this[RelationData] || {};
899
+ }
900
+ }
901
+ /**
902
+ * Define a junction link between two models in a n:m relation
903
+ *
904
+ * This is used to define the junction model in a n:m relation with additional data
905
+ * It does not require to have a symetric relation on the other side
906
+ */
907
+ export class JunctionLink {
908
+ constructor() {
909
+ this[_d] = ["linkA", "linkB"];
910
+ }
911
+ static { _d = WEBDA_PRIMARY_KEY; }
912
+ /**
913
+ * Return the composite primary key (linkA + linkB)
914
+ * @returns the composite primary key object
915
+ */
916
+ getPrimaryKey() {
917
+ return { linkA: this.linkA.getPrimaryKey(), linkB: this.linkB.getPrimaryKey() };
918
+ }
919
+ /** @override */
920
+ load(params) {
921
+ return this;
922
+ }
923
+ /**
924
+ * Return a string UID combining both link keys
925
+ * @returns the combined UID string
926
+ */
927
+ getUUID() {
928
+ return `${this.linkA.getPrimaryKey()}_${this.linkB.getPrimaryKey()}`;
929
+ }
930
+ /**
931
+ * Return the proxied version of this model (identity for JunctionLink)
932
+ * @returns this instance
933
+ */
934
+ toProxy() {
935
+ return this;
936
+ }
937
+ }
938
+ //# sourceMappingURL=relations.js.map