@liminalfunctions/framework-vitamins 1.0.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.
@@ -0,0 +1,426 @@
1
+ import { v4 as uuid } from 'uuid'
2
+ import { App } from 'vue'
3
+ import { generated_collection_interface, generated_document_interface, Infer_Collection_Returntype, result } from './type_generated_collection.js'
4
+
5
+ type query_operation = "get" | "query";
6
+ type child_generator<T extends result> = (result: T) => Query;
7
+
8
+
9
+
10
+ class Document {
11
+ id: string;
12
+
13
+ vitamins: Vitamins;
14
+ children: Set<string>;
15
+ parents: Set<string>;
16
+ reference: generated_collection_interface<result> | generated_document_interface<result>;
17
+ document: result;
18
+
19
+ constructor(vitamins: Vitamins, reference: generated_collection_interface<result> | generated_document_interface<result>, document: result) {
20
+ this.vitamins = vitamins;
21
+ this.children = new Set();
22
+ this.parents = new Set();
23
+
24
+ this.reference = reference;
25
+ this.document = document;
26
+ this.id = document._id;
27
+ }
28
+
29
+ unlink_parent(id: string) {
30
+ this.parents.delete(id);
31
+ }
32
+ }
33
+
34
+ class Query {
35
+ id: string;
36
+ vitamins: Vitamins;
37
+ children: Set<string>;
38
+ parents: Set<string>;
39
+
40
+ reference: generated_collection_interface<result> | generated_document_interface<result>;
41
+ collection_path: string;
42
+ operation: query_operation;
43
+ document_id?: string;
44
+ query_parameters?: any;
45
+
46
+ child_generators: child_generator<result>[];
47
+ has_run: boolean;
48
+
49
+ constructor(vitamins: Vitamins, reference: generated_collection_interface<result> | generated_document_interface<result>, argument?: object, child_generators: child_generator<result>[] = []){
50
+ this.id = uuid();
51
+ vitamins._debug(`constructing query ${reference.collection_id} ${this.id}`)
52
+ this.children = new Set();
53
+ this.parents = new Set();
54
+ this.vitamins = vitamins;
55
+ this.reference = reference;
56
+ this.child_generators = child_generators;
57
+ // if the reference has a query method, then it's a collection reference and we should do query operations on it
58
+ if((reference as generated_collection_interface<result>).query) {
59
+ this.query_parameters = argument as any;
60
+ this.collection_path = this.reference.path.join('/')
61
+ this.operation = 'query';
62
+ } else if((reference as generated_document_interface<result>).get) {// if the reference has a get method, then it's a document reference and we should do get operations on it
63
+ this.document_id = (reference as generated_document_interface<result>).document_id;
64
+ this.collection_path = [...this.reference.path, this.document_id].join('/')
65
+ this.operation = 'get';
66
+ } else {
67
+ throw new Error(`reference is not a collection reference or a query reference. Reexamine that argument.`)
68
+ }
69
+ this.has_run = false;
70
+ }
71
+
72
+ async rerun() {
73
+ this.vitamins._debug(`RERUNNING QUERY`)
74
+ this.has_run = false;
75
+ await this._fetch();
76
+ }
77
+
78
+ async run(run_from_root: boolean = true): Promise<Query> {
79
+ this.vitamins._debug(`running ${this.reference.collection_id}`)
80
+
81
+ // automatically replace yourself with an existing query if appliccable
82
+ let self = this.vitamins._find_existing_query(this) ?? this;
83
+ if(self.id !== this.id) { this.vitamins._debug('replacing self with doppleganger')}
84
+
85
+ if(run_from_root && !self.parents.has('root')){
86
+ self.parents.add('root');
87
+ }
88
+
89
+ self.vitamins._add_query(self);
90
+
91
+ // if we already had that query,....
92
+ if(self.id !== this.id){
93
+ // if any generators were specified, look for new ones and add them.
94
+ for(let generator of this.child_generators) {
95
+ if(!self.child_generators.includes(generator)){
96
+ this.vitamins._debug(`ADDING CHILD GENERATOR`)
97
+ this.vitamins._debug(generator)
98
+ self.child_generators.push(generator);
99
+ }
100
+ }
101
+
102
+ // generate the child queries for the new generators, since that wouldn't otherwise happen.
103
+ for(let child_id of this.children) {
104
+ let document = this.vitamins.documents.get(child_id);
105
+ let generated_child_queries = this.vitamins._generate_child_queries(document);
106
+ generated_child_queries.forEach(ele => this.vitamins._add_query(ele));
107
+ generated_child_queries.forEach(ele => ele.run());
108
+ }
109
+ } else {
110
+ await self._fetch();
111
+ }
112
+
113
+ return self;
114
+ }
115
+
116
+ async _fetch(){
117
+ if(this.has_run){ return; }
118
+ this.has_run = true;
119
+ try {
120
+ if(this.operation === 'get'){
121
+ let reference = this.reference as generated_document_interface<result>;
122
+ // TODO: how do I want to handle errors? This clearly needs to be in a try-catch.
123
+ let result = await reference.get();
124
+
125
+ this.vitamins._update_data(reference, result._id, result, this);
126
+ } else if(this.operation === 'query'){
127
+ let reference = this.reference as generated_collection_interface<result>;
128
+ // TODO: how do I want to handle errors? This clearly needs to be in a try-catch.
129
+ let results = await reference.query(this.query_parameters);
130
+ for(let result of results){
131
+ this.vitamins._update_data(reference, result._id, result, this);
132
+ }
133
+ }
134
+ } catch(err){
135
+ return Promise.reject(err);
136
+ }
137
+ }
138
+
139
+ link_child(document: Document) {
140
+ this.children.add(document.id);
141
+ document.parents.add(this.id);
142
+ }
143
+
144
+ link_parent(document: Document) {
145
+ this.parents.add(document.id);
146
+ document.children.add(this.id);
147
+ }
148
+
149
+ unlink_child(id: string) {
150
+ this.children.delete(id);
151
+ }
152
+
153
+ unlink_parent(id: string) {
154
+ this.parents.delete(id);
155
+ }
156
+
157
+ equals(query: Query) {
158
+ if(this === query){ return true;}
159
+ if(query.operation !== this.operation){ return false; }
160
+ if(query.collection_path !== this.collection_path) { return false; }
161
+ if(query.document_id !== this.document_id) { return false; }
162
+ if(this.query_parameters || query.query_parameters) {
163
+ if(!this.query_parameters || !query.query_parameters) { return false; }
164
+ if(!compare_query_parameters(query.query_parameters as Object, this.query_parameters as Object)) { return false; }
165
+ }
166
+ return true;
167
+ }
168
+
169
+ static find_query(queries: Query[], target: Query){
170
+ for(let query of queries){
171
+ if(query.equals(target)) { return query; }
172
+ }
173
+ return undefined;
174
+ }
175
+ }
176
+
177
+ function compare_query_parameters(a: object, b: object): boolean {
178
+ let key_value_a = Object.entries(a);
179
+ if(key_value_a.length !== Object.keys(b).length) { return false; }
180
+ for(let [key, value_a] of key_value_a) {
181
+ //@ts-expect-error
182
+ let value_b = b[key] as any;
183
+ if(typeof value_a !== typeof value_b) { return false; }
184
+ if(Array.isArray(value_a)) {
185
+ if(!Array.isArray(value_b)){ return false; }
186
+ if(!compare_array(value_a, value_b)){ return false; }
187
+ } else if(value_a !== value_b){
188
+ return false;
189
+ }
190
+ }
191
+
192
+ return true;
193
+ }
194
+
195
+ function compare_array(a: any[], b: any[]){
196
+ if(a.length !== b.length){ return false; }
197
+ for(let q = 0; q < a.length; q++){
198
+ if(a[q] !== b[q]){ return false;}
199
+ }
200
+ return true;
201
+ }
202
+
203
+ function quickprint(query: Query){
204
+ return {
205
+ id: query.id,
206
+ collection: query.reference.collection_id,
207
+ query_parameters: query.query_parameters
208
+ }
209
+ }
210
+
211
+ export class Vitamins {
212
+ vue: App
213
+ documents: Map<string, Document> // document id -> document
214
+ all_queries: Map<string, Query>
215
+ queries_by_collection: Map<string, Set<Query>>// collection id -> document[]
216
+ debug_on: boolean;
217
+
218
+ constructor(vue: App) {
219
+ this.vue = vue;
220
+ this.documents = new Map();
221
+ this.queries_by_collection = new Map();
222
+ this.all_queries = new Map()
223
+ this.debug_on = false;
224
+ }
225
+
226
+ query<Collection extends generated_collection_interface<result>>(collection: Collection, query_parameters: any, ...generators: child_generator<Infer_Collection_Returntype<Collection>>[]): Query {
227
+ // if queries_by_collection does not yet have a key for the relevant collection, create one.
228
+ if(!this.queries_by_collection.has(collection.collection_id)){ this.queries_by_collection.set(collection.collection_id, new Set()); }
229
+
230
+ // if the created query already exists within the system, set up to return the existing query instead
231
+ let generated_query = new Query(this, collection, query_parameters, generators);
232
+
233
+ //return query;
234
+ return generated_query;
235
+ }
236
+
237
+ delete_document_from_external(document_id: string) {
238
+ let document = this.documents.get(document_id);
239
+ if(!document) { return; }
240
+ let parent_queries = Array.from(document.parents).map(ele => this.all_queries.get(ele));
241
+ document.parents.clear();
242
+ let child_queries = Array.from(document.children).map(ele => this.all_queries.get(ele));
243
+ document.children.clear();
244
+ parent_queries.forEach(ele => ele.unlink_child(document_id));
245
+ child_queries.forEach(ele => ele.unlink_parent(document_id));
246
+ this._cleanup([...parent_queries, ...child_queries], [document]);
247
+ }
248
+
249
+ update_document_from_external(document_id: string, data: result) {
250
+ return this._update_data(undefined, document_id, data);
251
+ }
252
+
253
+ _debug(...print: any[]) {
254
+ if(this.debug_on){ console.log(print); }
255
+ }
256
+
257
+ _find_existing_query(query: Query) {
258
+ let collection_queries = this.queries_by_collection.get(query.reference.collection_id);
259
+ let existing_query = Query.find_query(Array.from(collection_queries), query);
260
+ return existing_query;
261
+ }
262
+
263
+ _add_query(query: Query) {
264
+ this._debug(`attaching query ${query.id}`)
265
+ // if queries_by_collection does not yet have a key for the relevant collection, create one.
266
+ if(!this.queries_by_collection.has(query.reference.collection_id)){
267
+ this.queries_by_collection.set(query.reference.collection_id, new Set());
268
+ }
269
+
270
+ // add the query to the maps and sets
271
+ let queries = this.queries_by_collection.get(query.reference.collection_id);
272
+ queries.add(query);
273
+ this.all_queries.set(query.id, query);
274
+ }
275
+
276
+ _delete_query(query: Query) {
277
+ this.queries_by_collection.get(query.reference.collection_id).delete(query);
278
+ this.all_queries.delete(query.id);
279
+ }
280
+
281
+ _add_document(document: Document) {
282
+ this.documents.set(document.document._id, document);
283
+ }
284
+
285
+ // TODO: do I need to be accepting an array of documents so that I can link/unlink all of them?
286
+ _update_data(reference: generated_collection_interface<result> | generated_document_interface<result> | undefined, document_id: string, data: result, query?: Query) {
287
+ // if this document doesn't already exist, create it.
288
+ let document = this.documents.get(document_id);
289
+ if(!document && reference) {
290
+ document = new Document(this, reference, data);
291
+ this._add_document(document);
292
+ }
293
+ if(!document && !reference){ return; }
294
+
295
+ this._debug(`updating data for a ${document.reference.collection_id} ${document_id}`);
296
+
297
+ // update the data for the document
298
+ document.document = data;
299
+
300
+ // make the query a parent of the document. Query's parent_of method already checks to make sure it's
301
+ // not already a parent, so you don't need to do that again here.
302
+ // TODO: unlink documents!
303
+ if(query){
304
+ query.link_child(document);
305
+ }
306
+
307
+
308
+ // remove doc's existing children, because we're regenerating all the query connections
309
+ let document_previous_children = Array.from(document.children);
310
+ document.children.clear();
311
+
312
+ for(let child_query_id of document_previous_children) {
313
+ this.all_queries.get(child_query_id).unlink_parent(document.id);
314
+ }
315
+
316
+ // get the full set of parent queries so that we can re-generate any child queries.
317
+ // let generated_child_queries = this._generate_child_queries(parent_query);
318
+ let generated_child_queries = this._generate_child_queries(document);
319
+ generated_child_queries.forEach(ele => this._add_query(ele));
320
+ generated_child_queries.forEach(ele => this._debug(quickprint(ele)))
321
+
322
+ generated_child_queries.forEach(ele => ele.run(false));
323
+
324
+ let test_queries_for_deletion: Query[] = document_previous_children.map(query_id => this.all_queries.get(query_id));
325
+ let bugfind = Array.from(document_previous_children).filter(id => !this.all_queries.has(id))
326
+ if(bugfind.length > 0){ this._debug(`BREAKING ID:`); this._debug(bugfind); }
327
+
328
+ this._cleanup(test_queries_for_deletion, []);
329
+
330
+
331
+
332
+ /*
333
+ Clone the response data to prevent any funkyness if it gets changed in the frontend code,
334
+ and then load it into Vue.
335
+ */
336
+ let cloned_data = structuredClone(data);
337
+
338
+ //@ts-expect-error
339
+ if(!this.vue[document.reference.collection_id]){
340
+ throw new Error(`when updating ${document.reference.collection_id}, found that the vue app does not have a ${document.reference.collection_id} key`);
341
+ }
342
+
343
+ //@ts-expect-error
344
+ if(!(this.vue[document.reference.collection_id] instanceof Map)){
345
+ throw new Error(`when updating ${document.reference.collection_id}, found that the vue app key ${document.reference.collection_id} is not a map. It should be a Map<string, ${document.reference.collection_id}>`);
346
+ }
347
+
348
+ //@ts-expect-error
349
+ (this.vue[document.reference.collection_id] as Map).set(document_id, cloned_data);
350
+ }
351
+
352
+ _generate_child_queries(document: Document): Query[] {
353
+ // each query produces documents. So if you get an institution, the query will
354
+ // produce institution documents. These documents are registered as children of the query.
355
+ // each query also has the ability to produce more queries, which are registered as query generators.
356
+ // So, the general process to generate child queries is to find all the child documents
357
+ // of a query, call the query generator for each child document, and--ifs
358
+ // the query hasn't been generated before--attach it to the child document.
359
+
360
+ // keep a list of all the queries generated, so that we can return them at the end of this process
361
+ let all_generated_child_queries: Query[] = [];
362
+
363
+ for(let query_parent_id of document.parents) {
364
+ let query_parent = this.all_queries.get(query_parent_id);
365
+
366
+ let generated_child_queries = query_parent.child_generators.map(generator => generator(document.document));
367
+ for(let q = 0; q < generated_child_queries.length; q++){
368
+ // if we already had the child query, use the existing one instead of the new one
369
+ let generated_child_query = generated_child_queries[q];
370
+ let query = this._find_existing_query(generated_child_query) ?? generated_child_query;
371
+ if(generated_child_query.id !== query.id ){ generated_child_queries[q] = query; }
372
+ //this._add_query(generated_child_query);
373
+ generated_child_query.link_parent(document);
374
+ }
375
+ all_generated_child_queries.push(...generated_child_queries);
376
+ }
377
+ return Array.from(new Set(all_generated_child_queries));
378
+ }
379
+
380
+ _cleanup(queries: Query[], documents: Document[]){
381
+ let check_queries_queue = queries.slice();
382
+ let check_documents_queue = documents.slice();
383
+
384
+ while(check_queries_queue.length > 0 || check_documents_queue.length > 0) {
385
+ while(check_queries_queue.length > 0){
386
+ let query = check_queries_queue.pop();
387
+ if(query.parents.size > 0){ continue; }
388
+
389
+ for(let child_id of query.children){
390
+ let child = this.documents.get(child_id);
391
+ check_documents_queue.push(child);
392
+ child.unlink_parent(query.id);
393
+ }
394
+
395
+ // remove the query from our set of queries
396
+ this._delete_query(query);
397
+ }
398
+
399
+ while(check_documents_queue.length > 0){
400
+ let document = check_documents_queue.pop();
401
+ if(document.parents.size > 0){ continue; }
402
+
403
+ for(let child_id of document.children){
404
+ let child = this.all_queries.get(child_id);
405
+ check_queries_queue.push(this.all_queries.get(child_id));
406
+ child.unlink_parent(document.id);
407
+ }
408
+
409
+ this.documents.delete(document.id);
410
+ //@ts-expect-error
411
+ if(!this.vue[document.reference.collection_id]){
412
+ throw new Error(`when updating ${document.reference.collection_id}, found that the vue app does not have a ${document.reference.collection_id} key`)
413
+ };
414
+
415
+ //@ts-expect-error
416
+ if(!this.vue[document.reference.collection_id] instanceof Map){
417
+ throw new Error(`when updating ${document.reference.collection_id}, found that the vue app key ${document.reference.collection_id} is not a map. It should be a Map<string, ${document.reference.collection_id}>`)
418
+ };
419
+ //@ts-expect-error
420
+ this.vue[document.reference.collection_id].delete(document.id);
421
+ }
422
+ }
423
+ }
424
+
425
+ // TOOD: I need a method that updates/deletes the document from an external source, so that I can reimplement the Synchronizer.
426
+ }