@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.
- package/LICENSE +166 -0
- package/README.md +146 -2
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -0
- package/lib/mock.d.ts +72 -0
- package/lib/mock.d.ts.map +1 -0
- package/lib/mock.js +83 -0
- package/lib/mock.js.map +1 -0
- package/lib/model.model.d.ts +286 -0
- package/lib/model.model.d.ts.map +1 -0
- package/lib/model.model.js +336 -0
- package/lib/model.model.js.map +1 -0
- package/lib/relations.d.ts +705 -0
- package/lib/relations.d.ts.map +1 -0
- package/lib/relations.js +938 -0
- package/lib/relations.js.map +1 -0
- package/lib/repositories/abstract.d.ts +138 -0
- package/lib/repositories/abstract.d.ts.map +1 -0
- package/lib/repositories/abstract.js +280 -0
- package/lib/repositories/abstract.js.map +1 -0
- package/lib/repositories/conformance.d.ts +40 -0
- package/lib/repositories/conformance.d.ts.map +1 -0
- package/lib/repositories/conformance.js +38 -0
- package/lib/repositories/conformance.js.map +1 -0
- package/lib/repositories/event.d.ts +101 -0
- package/lib/repositories/event.d.ts.map +1 -0
- package/lib/repositories/event.js +228 -0
- package/lib/repositories/event.js.map +1 -0
- package/lib/repositories/hooks.d.ts +88 -0
- package/lib/repositories/hooks.d.ts.map +1 -0
- package/lib/repositories/hooks.js +105 -0
- package/lib/repositories/hooks.js.map +1 -0
- package/lib/repositories/memory.d.ts +262 -0
- package/lib/repositories/memory.d.ts.map +1 -0
- package/lib/repositories/memory.js +574 -0
- package/lib/repositories/memory.js.map +1 -0
- package/lib/repositories/repository.d.ts +238 -0
- package/lib/repositories/repository.d.ts.map +1 -0
- package/lib/repositories/repository.js +19 -0
- package/lib/repositories/repository.js.map +1 -0
- package/lib/storable.d.ts +210 -0
- package/lib/storable.d.ts.map +1 -0
- package/lib/storable.js +76 -0
- package/lib/storable.js.map +1 -0
- package/lib/types.d.ts +230 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +12 -0
- package/lib/types.js.map +1 -0
- package/package.json +60 -3
package/lib/relations.js
ADDED
|
@@ -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
|