goby-database 1.0.8 → 2.0.9

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/src/index.ts ADDED
@@ -0,0 +1,1251 @@
1
+ import Database from 'better-sqlite3';
2
+ import {defined,partial_relation_match,full_relation_match,can_have_multiple_values,junction_col_name, side_match,two_way,edit_has_valid_sides,readable_edit} from './utils.js';
3
+ import type { Database as DatabaseType, Statement } from 'better-sqlite3';
4
+ import type {
5
+ SQLTableType,
6
+ SQLClassListRow,
7
+ SQLJunctonListRow,
8
+ JunctionSides,
9
+ RelationshipSideBase,
10
+ JunctionList,
11
+ ClassList,
12
+ ClassMetadata,
13
+ Property,
14
+ DataType,
15
+ ItemRelationSide,
16
+ SQLApplicationWindow,
17
+ ApplicationWindow,
18
+ SQLWorkspaceBlockRow,
19
+ WorkspaceBlock,
20
+ ClassData,
21
+ ClassRow,
22
+ ClassEdit,
23
+ RelationEdit,
24
+ PropertyEdit,
25
+ MaxValues,
26
+ PropertyType,
27
+ RelationProperty,
28
+ DataProperty,
29
+ RelationEditValidSides
30
+ } from './types.js';
31
+
32
+ const text_data_types=['string','resource'];
33
+ const real_data_types=['number'];
34
+
35
+ export default class Project{
36
+ db:DatabaseType;
37
+
38
+ run:{
39
+ [key:string]:Statement;
40
+ get_all_classes:Statement<[],SQLClassListRow>;
41
+ get_junctionlist:Statement<[],SQLJunctonListRow>;
42
+ get_junctions_matching_property:Statement<{class_id:number,prop_id:number | null},{id:number,sides:string}>;
43
+ get_windows:Statement<[],SQLApplicationWindow>;
44
+ get_class_id:Statement<[string],{id:number}>;
45
+ };
46
+
47
+ class_cache:ClassList=[];
48
+ item_cache:{
49
+ class_id:number,
50
+ items:ClassRow[]
51
+ }[]=[]
52
+ junction_cache:JunctionList=[];
53
+
54
+ constructor(source:string){
55
+ this.db= new Database(source);
56
+
57
+
58
+ //checks if goby has been initialized, initializes if not
59
+ const goby_init=this.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='system_root'`).get();
60
+ if(!goby_init){
61
+ console.log('initializing goby database');
62
+ this.init();
63
+ }else{
64
+ console.log('opened goby database');
65
+ }
66
+
67
+ //prepared statements with arguments so my code isn't as verbose elsewhere
68
+ this.run={
69
+ begin:this.db.prepare('BEGIN IMMEDIATE'),
70
+ commit:this.db.prepare('COMMIT'),
71
+ rollback:this.db.prepare('ROLLBACK'),
72
+ create_item:this.db.prepare('INSERT INTO system_root(type,value) VALUES (@type, @value)'),
73
+ get_junctionlist:this.db.prepare<[],SQLJunctonListRow>(`
74
+ SELECT
75
+ id,
76
+ json_array(
77
+ json_object('class_id',side_0_class_id,'prop_id',side_0_prop_id),
78
+ json_object('class_id',side_1_class_id,'prop_id',side_1_prop_id)
79
+ ) AS sides,
80
+ metadata FROM system_junctionlist`),
81
+ get_junctions_matching_property:this.db.prepare<{class_id:number,prop_id:number | null},{id:number,sides:string}>(`
82
+ SELECT
83
+ id,
84
+ json_array(
85
+ json_object('class_id',side_0_class_id,'prop_id',side_0_prop_id),
86
+ json_object('class_id',side_1_class_id,'prop_id',side_1_prop_id)
87
+ ) AS sides,
88
+ metadata
89
+ FROM system_junctionlist
90
+ WHERE (side_0_class_id = @class_id AND side_0_prop_id = @prop_id)
91
+ OR (side_1_class_id = @class_id AND side_1_prop_id = @prop_id)
92
+ `),
93
+ get_class:this.db.prepare(`SELECT name, metadata FROM system_classlist WHERE id = ?`),
94
+ get_class_id:this.db.prepare<[string],{id:number}>(`SELECT id FROM system_classlist WHERE name = ?`),
95
+ get_all_classes:this.db.prepare<[],SQLClassListRow>(`SELECT id, name, metadata FROM system_classlist`),
96
+ save_class_meta:this.db.prepare(`UPDATE system_classlist set metadata = ? WHERE id = ?`),
97
+ update_window:this.db.prepare(`UPDATE system_windows set open = @open, type=@type, metadata = @meta WHERE id = @id`),
98
+ create_window: this.db.prepare(`INSERT INTO system_windows (type,open, metadata) VALUES (@type, @open, @meta)`),
99
+ get_windows:this.db.prepare<[],SQLApplicationWindow>(`SELECT id, type, open, metadata FROM system_windows`)
100
+ }
101
+
102
+
103
+
104
+
105
+
106
+ this.refresh_caches(['classlist','items','junctions']);
107
+
108
+
109
+ // commenting this out until I figure out my transaction / one-step-undo functionality
110
+ //if I understand transactions correctly, a new one will begin with every user action while committing the one before, meaning I'll need to have the first begin here
111
+ // this.run.begin.run();
112
+
113
+ }
114
+
115
+ get_latest_table_row_id(table_name:string):number | null{
116
+ let db_get=this.db.prepare<[],{id:number}>(`SELECT last_insert_rowid() AS id FROM ${table_name}`).get();
117
+ // if no row found, silently return null
118
+ if(!db_get) return null;
119
+ let id=db_get.id;
120
+ return id;
121
+ }
122
+
123
+ init(){
124
+ //System table to contain all items in the project.
125
+ this.create_table('system','root',[
126
+ 'id INTEGER NOT NULL PRIMARY KEY',
127
+ 'type TEXT',
128
+ 'value TEXT'
129
+ ]);
130
+
131
+ //System table to contain metadata for all classes created by user
132
+ this.create_table('system','classlist',['id INTEGER NOT NULL PRIMARY KEY','name TEXT','metadata TEXT']);
133
+ //System table to contain all the junction tables and aggregate info about relations
134
+ this.create_table('system','junctionlist',[
135
+ 'id INTEGER NOT NULL PRIMARY KEY',
136
+ 'side_0_class_id INTEGER NOT NULL',
137
+ 'side_0_prop_id INTEGER',
138
+ 'side_1_class_id INTEGER NOT NULL',
139
+ 'side_1_prop_id INTEGER',
140
+ 'metadata TEXT'
141
+ ]);
142
+ //System table to contain generated image data
143
+ this.create_table('system','images',['file_path TEXT','img_type TEXT','img BLOB']);
144
+
145
+ // window "open" is a boolean stored as 0 or 1
146
+ this.create_table('system','windows',[
147
+ 'id INTEGER NOT NULL PRIMARY KEY',
148
+ 'type TEXT',
149
+ 'open INTEGER',
150
+ 'metadata TEXT'
151
+ ]);
152
+
153
+
154
+ this.db.prepare(`INSERT INTO system_windows
155
+ (type, open, metadata)
156
+ VALUES
157
+ ('home',0,'${JSON.stringify({pos:[null,null], size:[540,400]})}'),
158
+ ('hopper',0,'${JSON.stringify({pos:[null,null], size:[300,400]})}')`).run();
159
+
160
+ // [TO ADD: special junction table for root items to reference themselves in individual relation]
161
+ this.create_table('system','junction_root',[
162
+ 'id_1 INTEGER',
163
+ 'id_2 INTEGER',
164
+ 'metadata TEXT'
165
+ ]);
166
+
167
+
168
+
169
+
170
+
171
+ }
172
+
173
+
174
+ refresh_caches(caches:('classlist' | 'items' | 'junctions')[]){
175
+ if(caches.includes('classlist')){
176
+ this.class_cache=this.retrieve_all_classes();
177
+ }
178
+
179
+ if(caches.includes('items')){
180
+ let refreshed_items=[];
181
+ for(let class_data of this.class_cache){
182
+ let items=this.retrieve_class_items({class_id:class_data.id});
183
+ refreshed_items.push({
184
+ class_id:class_data.id,
185
+ items
186
+ })
187
+ class_data.items=items;
188
+ }
189
+ this.item_cache=refreshed_items;
190
+ }
191
+
192
+ if(caches.includes('junctions')){
193
+ this.junction_cache=this.get_junctions();
194
+ }
195
+ }
196
+
197
+
198
+ create_table(type:SQLTableType,name:string | number,columns:string[]){
199
+ //type will pass in 'class', 'system', or 'junction' to use as a name prefix
200
+ //columns is an array of raw SQL column strings
201
+
202
+ let columns_string=columns.join(',');
203
+
204
+ //brackets to allow special characters in user-defined names
205
+ // validation test: what happens if there are brackets in the name?
206
+ const sqlname=type=='class'?`[class_${name}]`:type=='properties'?`class_${name}_properties`:`${type}_${name}`;
207
+ let create_statement=`CREATE TABLE ${sqlname}(
208
+ ${columns_string}
209
+ )`;
210
+ this.db.prepare(create_statement).run();
211
+ }
212
+
213
+ action_create_class(name:string ):number{
214
+
215
+ //a class starts with these basic columns
216
+ let columns=[
217
+ 'system_id INTEGER UNIQUE',
218
+ 'system_order REAL',
219
+ 'user_name TEXT',
220
+ `FOREIGN KEY(system_id) REFERENCES system_root(id)`
221
+ ];
222
+
223
+ this.create_table('class',name,columns);
224
+
225
+ const class_meta:ClassMetadata={
226
+ style:{
227
+ color: '#b5ffd5'
228
+ }
229
+ };
230
+
231
+ // create entry for class in classlist
232
+ this.db.prepare(`INSERT INTO system_classlist (name, metadata) VALUES ('${name}','${JSON.stringify(class_meta)}')`).run();
233
+
234
+ //get the id of the newest value
235
+ const class_id=this.db.prepare<[],{id:number}>('SELECT id FROM system_classlist ORDER BY id DESC').get()?.id;
236
+ if(class_id==undefined) throw new Error('Something went wrong when generating new class.');
237
+
238
+ // create a table to record properties of this class
239
+ this.create_table('properties',class_id,[
240
+ `id INTEGER NOT NULL PRIMARY KEY`,
241
+ `system_order REAL`,
242
+ `name TEXT NOT NULL`,
243
+ `type`,
244
+ `data_type TEXT`,
245
+ `max_values INTEGER`,
246
+ `metadata TEXT`
247
+ ])
248
+
249
+ this.refresh_caches(['classlist']);
250
+
251
+ // add an entry in the property table for the default "name" property.
252
+ this.action_add_data_property({class_id,name:'name',data_type:'string',max_values:1,create_column:false})
253
+
254
+
255
+ return class_id;
256
+
257
+ }
258
+
259
+
260
+ action_add_data_property({
261
+ class_id,
262
+ name,
263
+ data_type,
264
+ max_values,
265
+ create_column=true
266
+ }:{
267
+ class_id:number,
268
+ name:string,
269
+ data_type:DataType,
270
+ max_values:MaxValues,
271
+ create_column?:boolean
272
+ }){
273
+ // 1. Add property to property table ---------------------------------------------------
274
+
275
+ let property_table=`class_${class_id}_properties`;
276
+ const system_order=this.get_next_order(property_table);
277
+
278
+ this.db.prepare(`INSERT INTO ${property_table} (name,type,data_type,max_values,metadata,system_order) VALUES (@name,@type,@data_type,@max_values,@metadata,@system_order)`).run({
279
+ name,
280
+ type:'data',
281
+ data_type,
282
+ max_values,
283
+ metadata:'{}',
284
+ system_order
285
+ })
286
+ // let prop_id=this.get_latest_table_row_id(property_table);
287
+
288
+ // 2. Add column to class table ------------------------------------------------
289
+ if(create_column){
290
+ let class_data=this.class_cache.find(a=>a.id==class_id);
291
+ if(class_data == undefined) throw new Error('Cannot find class in class list.');
292
+ let class_name=class_data.name;
293
+
294
+ let sql_data_type='';
295
+
296
+ if(can_have_multiple_values(max_values)||text_data_types.includes(data_type)){
297
+ //multiple for data means a stringified array no matter what it is
298
+ sql_data_type='TEXT';
299
+ }else if(real_data_types.includes(data_type)){
300
+ sql_data_type='REAL';
301
+ }
302
+
303
+ //create property in table
304
+ let command_string=`ALTER TABLE [class_${class_name}] ADD COLUMN [user_${name}] ${sql_data_type};`;
305
+ this.db.prepare(command_string).run();
306
+ }
307
+ this.refresh_caches(['classlist']);
308
+
309
+ }
310
+
311
+ action_add_relation_property(class_id:number,name:string,max_values:MaxValues){
312
+
313
+ let property_table=`class_${class_id}_properties`;
314
+
315
+ const system_order=this.get_next_order(property_table);
316
+
317
+ this.db.prepare(`INSERT INTO ${property_table} (name,type,max_values,metadata,system_order) VALUES (@name,@type,@max_values,@metadata,@system_order)`).run({
318
+ name,
319
+ type:'relation',
320
+ max_values,
321
+ metadata:'{}',
322
+ system_order
323
+ })
324
+
325
+ let prop_id=this.get_latest_table_row_id(property_table);
326
+
327
+ if(defined(prop_id)){
328
+ this.refresh_caches(['classlist']);
329
+ return prop_id;
330
+ }else{
331
+ throw Error('Something went wrong registering a property for the class')
332
+ }
333
+
334
+ // WONDERING WHERE THE RELATIONSHIP TARGET LOGIC IS?
335
+ // this info is not stored directly on the property, but as a relationship/junction record
336
+ // this is processed in action_edit_class_schema, which handles relation changes/additions concurrently for all the classes they affect.
337
+
338
+ }
339
+
340
+
341
+ delete_property(class_id:number,prop_id:number){
342
+ // NOTE: I need to enforce that you can’t delete the default "name" property
343
+
344
+ // this function is meant to be used within a flow where the relations that need to change as a result of this deletion are already kept track of
345
+
346
+ let class_data=this.class_cache.find(a=>a.id==class_id);
347
+ if(!class_data) throw new Error('Cannot locate class to delete property from.');
348
+ let property=class_data.properties.find(a=>a.id==prop_id);
349
+ if(!property) throw new Error('Cannot locate property to delete.');
350
+
351
+ // delete it from property record
352
+ this.db.prepare(`DELETE FROM class_${class_id}_properties WHERE id = ${prop_id}`).run();
353
+
354
+ if(property.type=='data'){
355
+ // drop column from class table if of type data
356
+ this.db.prepare(`ALTER TABLE class_${class_id} DROP COLUMN [user_${property.name}]`);
357
+ }
358
+
359
+ this.refresh_caches(['classlist']);
360
+
361
+ }
362
+
363
+
364
+ get_junctions(){
365
+ let junction_list_sql=this.run.get_junctionlist.all();
366
+ let junction_list_parsed=junction_list_sql.map(a=>{
367
+ let sides=JSON.parse(a.sides) as JunctionSides;
368
+ return {
369
+ ...a,
370
+ sides
371
+ }
372
+ });
373
+ return junction_list_parsed;
374
+ }
375
+
376
+ action_edit_class_schema({
377
+ class_edits =[],
378
+ property_edits = [],
379
+ relationship_edits = []
380
+ }:{
381
+ class_edits?:ClassEdit[],
382
+ property_edits?:PropertyEdit[],
383
+ relationship_edits?:RelationEdit[]
384
+ }){
385
+
386
+ // get the list of existing relationships
387
+
388
+ // loop over class changes and make/queue them as needed
389
+ for(let class_edit of class_edits){
390
+ switch(class_edit.type){
391
+ case 'create':
392
+ // NOTE: in the future, check and enforce that the class name is unique
393
+
394
+ // register the class and get the ID
395
+ let class_id=this.action_create_class(class_edit.class_name);
396
+ // find all the properties which reference this new class name, and set the class_id.
397
+ for(let prop_edit of property_edits){
398
+ // only a newly created prop would be missing a class id
399
+ if(prop_edit.type=='create'){
400
+ if(
401
+ (!defined(prop_edit.class_id)) &&
402
+ prop_edit.class_name== class_edit.class_name
403
+ ){
404
+ prop_edit.class_id=class_id;
405
+ }
406
+ }
407
+ }
408
+ // do the same for relations
409
+ for(let relationship_edit of relationship_edits){
410
+ if(relationship_edit.type=='create' || relationship_edit.type=='transfer'){
411
+ for(let side of relationship_edit.sides){
412
+ if(!side.class_id && side.class_name == class_edit.class_name){
413
+ side.class_id=class_id;
414
+ }
415
+ }
416
+ }
417
+
418
+ }
419
+ break;
420
+ case 'delete':
421
+ if(class_edit.class_id){
422
+ this.action_delete_class(class_edit.class_id);
423
+ // look for any relationships which will be affected by the deletion of this class, and queue deletion
424
+ for(let junction of this.junction_cache){
425
+ if(junction.sides.some(s=>s.class_id==class_edit.class_id)){
426
+ relationship_edits.push({
427
+ type:'delete',
428
+ id:junction.id
429
+ })
430
+ }
431
+ }
432
+ }else{
433
+ throw Error("ID for class to delete not provided");
434
+ }
435
+ break;
436
+ case 'modify_attribute':
437
+ // TBD, will come back to this after relation stuff is sorted
438
+ // this should be harmless, just key into the attribute of metadata and set the value as desired
439
+ break;
440
+ }
441
+ }
442
+
443
+
444
+ // loop over property changes
445
+ for(let prop_edit of property_edits){
446
+
447
+ switch(prop_edit.type){
448
+ case 'create':
449
+ // class ID should be defined in class creation loop
450
+ if(defined(prop_edit.class_id)){
451
+
452
+ // NOTE: in the future, check and enforce that the prop name is unique
453
+
454
+ // register the property
455
+ if(prop_edit.config.type == 'relation'){
456
+ const prop_id=this.action_add_relation_property(
457
+ prop_edit.class_id,
458
+ prop_edit.prop_name,
459
+ prop_edit.config.max_values
460
+ );
461
+
462
+ // look for any relations which match the class id and prop name
463
+ // set their prop ID to the newly created one.
464
+ for(let relationship_edit of relationship_edits){
465
+ if(relationship_edit.type=='create' || relationship_edit.type=='transfer'){
466
+ for(let side of relationship_edit.sides){
467
+ if(
468
+ side.class_id==prop_edit.class_id &&
469
+ !defined(side.prop_id) &&
470
+ side.prop_name==prop_edit.prop_name
471
+ ){
472
+ side.prop_id=prop_id;
473
+ }
474
+ }
475
+ }
476
+
477
+ }
478
+ }else if(prop_edit.config.type == 'data'){
479
+ // if it's a data prop, it just has to be registered in the class table and metadata
480
+ this.action_add_data_property({
481
+ class_id:prop_edit.class_id,
482
+ name:prop_edit.prop_name,
483
+ data_type:prop_edit.config.data_type,
484
+ max_values:prop_edit.config.max_values
485
+ });
486
+ }
487
+ }
488
+
489
+ break;
490
+ case 'delete':
491
+ const prop=this.class_cache.find(a=>a.id==prop_edit.class_id)?.properties?.find((a)=>a.id==prop_edit.prop_id);
492
+ if(prop&&prop.type=='relation'){
493
+ // queue the deletion or transfer of relations involving this prop
494
+
495
+ for(let junction of this.junction_cache){
496
+ let includes_prop=junction.sides.find(s=>{
497
+ return s.class_id==prop_edit.class_id&&s.prop_id==prop_edit.prop_id;
498
+ })
499
+ if(includes_prop){
500
+ let non_matching=junction.sides.find(s=>!(s.class_id==prop_edit.class_id&&s.prop_id==prop_edit.prop_id));
501
+
502
+ if(non_matching){
503
+
504
+ if(defined(non_matching?.prop_id)){
505
+ // if there is a prop on the other side of the relation,
506
+ // and (to add) if there is not a partial match create or transfer in relationship_edits
507
+ // queue a transfer to a one-sided relation
508
+
509
+ // NOTE: I need to see if this can create any kind of conflict with existing relationship edits
510
+ relationship_edits.push({
511
+ type:'transfer',
512
+ id:junction.id,
513
+ sides:junction.sides,
514
+ new_sides:[
515
+ non_matching,
516
+ {class_id:prop_edit.class_id}
517
+ ]
518
+ })
519
+ }else{
520
+ // if not, no reason to keep that relation around
521
+ relationship_edits.push({
522
+ type:'delete',
523
+ id:junction.id
524
+ })
525
+ }
526
+ }
527
+
528
+
529
+ }
530
+ }
531
+ }
532
+
533
+ // NOTE: I might un-encapsulate the create and delete functions, given they should only be used from within this function
534
+ this.delete_property(prop_edit.class_id,prop_edit.prop_id);
535
+
536
+
537
+ break;
538
+ case 'modify':
539
+ // TBD, will come back to this after relation stuff is sorted
540
+ // changing property metadata, not including relationship targets
541
+ // and making any necessary changes to cell values
542
+ break;
543
+ }
544
+
545
+
546
+ }
547
+
548
+ this.refresh_caches(['classlist']);
549
+
550
+ // find cases where relationships getting deleted can transfer their connections to relationships getting created
551
+ let consolidated_relationship_edits:RelationEditValidSides[]=this.consolidate_relationship_edits(relationship_edits);
552
+
553
+ for(let relationship_edit of consolidated_relationship_edits){
554
+ switch(relationship_edit.type){
555
+ case 'create':{
556
+ // create the corresponding junction table
557
+ let new_sides=relationship_edit.sides;
558
+ this.create_junction_table(new_sides);
559
+ }
560
+ break;
561
+ case 'delete':
562
+ // delete the corresponding junction table
563
+ this.delete_junction_table(relationship_edit.id);
564
+ break;
565
+ case 'transfer':{
566
+ let old_sides=relationship_edit.sides;
567
+ let new_sides=relationship_edit.new_sides;
568
+ // 1. create the new junction table
569
+ const junction_id=this.create_junction_table(new_sides);
570
+
571
+ // 2. copy over the rows from the old table
572
+ this.transfer_connections({
573
+ id:relationship_edit.id,
574
+ sides:old_sides
575
+ },{
576
+ id:junction_id,
577
+ sides:new_sides
578
+ })
579
+ }
580
+ break;
581
+ }
582
+ }
583
+
584
+ this.refresh_caches(['classlist','items','junctions']);
585
+
586
+ }
587
+
588
+
589
+ consolidate_relationship_edits(relationship_edits:RelationEdit[]):RelationEditValidSides[]{
590
+ let class_cache=this.class_cache;
591
+
592
+ let consolidated_relationship_edits:(RelationEditValidSides&{exclude?:boolean})[]=[];
593
+ const relation_order={transfer:1,create:2,delete:3};
594
+ const sort_edits=(a:RelationEdit,b:RelationEdit)=>relation_order[a.type] - relation_order[b.type];
595
+
596
+ let source_array:RelationEditValidSides[]=[...relationship_edits.filter(edit_has_valid_sides)];
597
+ source_array.sort(sort_edits);
598
+ for(let i=0;i<source_array.length;i++){
599
+ let relationship_edit=source_array[i];
600
+ switch(relationship_edit.type){
601
+ // all of these are added before anything else
602
+ case 'transfer':{
603
+ console.log(`Queing ${readable_edit(relationship_edit,class_cache)}`);
604
+
605
+ push_if_valid(relationship_edit);
606
+ // transferring the connections implies deleting the source, so we queue that deletion
607
+ // deletions only happen after all the transfers,
608
+ // so that multiple properties can copy from the same source.
609
+ let delete_queued=source_array.find(a=>a.type=='delete' && a.id==relationship_edit.id);
610
+ if(!delete_queued){
611
+ let del:RelationEditValidSides={
612
+ type:'delete',
613
+ id:relationship_edit.id
614
+ };
615
+ console.log(`Queuing ${readable_edit(del,class_cache)} after transfer`)
616
+ source_array.push(del)
617
+ }
618
+ }
619
+
620
+ break;
621
+
622
+ // these are processed after the transfers but before the deletes.
623
+ case 'create':{
624
+ let new_sides=relationship_edit.sides;
625
+ // check if there’s an existing relation that matches both classes and one property
626
+ let existing = this.junction_cache.find((r)=>{
627
+ return partial_relation_match(new_sides,r.sides);
628
+ })
629
+
630
+ // if there is an existing match
631
+ if(existing){
632
+ // look for a type:"delete" which deletes this relation
633
+ let delete_queued=source_array.find(a=>a.type=='delete' && a.id==existing.id);
634
+
635
+ if(delete_queued){
636
+ let new_transfer:RelationEditValidSides={
637
+ type:'transfer',
638
+ id:existing.id,
639
+ sides:existing.sides,
640
+ new_sides:new_sides
641
+ }
642
+
643
+ console.log(`Found valid ${readable_edit(new_transfer,class_cache)}`)
644
+ // if there’s a delete, push a transfer instead
645
+ push_if_valid(new_transfer)
646
+ }else{
647
+ console.log(`Ignoring ${readable_edit(relationship_edit,class_cache)}; Cannot create a second relationship between two classes involving the same property.`)
648
+ }
649
+ // if there’s not a delete, we ignore this edit because it’s invalid
650
+ }else{
651
+ // if it does not exist, add the type:"create" normally
652
+ push_if_valid(relationship_edit);
653
+ }
654
+ }
655
+ break;
656
+
657
+ // these are processed last, after the creates and transfers.
658
+ case 'delete':
659
+ // these are always processed at the very end
660
+ push_if_valid(relationship_edit);
661
+ break;
662
+ }
663
+ }
664
+
665
+ // lastly, filter out duplicates of the same partial match (has to be separate because it picks the most specific);
666
+ consolidated_relationship_edits=filter_best_of_partial_matches(consolidated_relationship_edits);
667
+
668
+ return consolidated_relationship_edits;
669
+
670
+ // ignores if it already exists in the consolidated list (deduplication)
671
+ // ignores if it targets a class/property that no longer exists
672
+ function push_if_valid(edit:RelationEditValidSides){
673
+ let edit_already_added;
674
+ let targets_exist=true;
675
+ switch(edit.type){
676
+ case 'create':{
677
+ edit_already_added=consolidated_relationship_edits.some(a=>{
678
+ return a.type=='create'
679
+ && full_relation_match(a.sides,edit.sides)
680
+ })
681
+
682
+ targets_exist=check_if_targets_exist(edit.sides);
683
+ }
684
+ break;
685
+ case 'delete':{
686
+ edit_already_added=consolidated_relationship_edits.some(a=>a.type=='delete'&&a.id==edit.id);
687
+ }
688
+ break;
689
+ case 'transfer':{
690
+ edit_already_added=consolidated_relationship_edits.some(a=>{
691
+ return a.type=='transfer'
692
+ && a.id==edit.id
693
+ && full_relation_match(a.new_sides,edit.new_sides);
694
+ })
695
+ targets_exist=check_if_targets_exist(edit.sides);
696
+ }
697
+ }
698
+
699
+ if(!(targets_exist && !edit_already_added)) console.log('Skipped invalid',edit,'\n targeting deleted class/property:',!targets_exist,'\n edit already added:',edit_already_added);
700
+ if(targets_exist && !edit_already_added) consolidated_relationship_edits.push(edit);
701
+ }
702
+
703
+ function check_if_targets_exist(sides:[RelationshipSideBase,RelationshipSideBase]){
704
+ for(let side of sides){
705
+ let class_for_side=class_cache.find((c)=>c.id==side.class_id);
706
+ if(!class_for_side){
707
+ return false;
708
+ }else if(defined(side.prop_id)){
709
+ let prop=class_for_side.properties.find(a=>a.id==side.prop_id);
710
+ if(!prop) return false;
711
+ }
712
+ }
713
+
714
+ return true;
715
+ }
716
+
717
+ // allow no more than one of each partial match (partial match = shares both classes and one property)
718
+ // privilege relations with properties on both sides (two way); else accept the first one in the list.
719
+ function filter_best_of_partial_matches(edits:(RelationEditValidSides & {exclude?:boolean})[]){
720
+ return edits.filter(edit=>{
721
+
722
+ if(edit.type=='delete') return true;
723
+ if(edit.exclude) return false;
724
+
725
+ // keeps track of whether or not to keep this item in filtered selection
726
+ let include=true;
727
+
728
+ let sides=edit.type=='transfer'?edit.new_sides:edit.sides;
729
+ let is_two_way=two_way(sides);
730
+
731
+ // look for partial matches in the array
732
+ for(let comparison of consolidated_relationship_edits){
733
+ if(comparison==edit || comparison.type == 'delete' || comparison.exclude){
734
+ // ignore if not applicable
735
+ continue;
736
+ }else{
737
+ let comparison_sides=comparison.type=='transfer'?comparison.new_sides:comparison.sides;
738
+ // check if it’s a partial match
739
+ if(partial_relation_match(comparison_sides,sides)){
740
+ // if so, check if the compared edit is two-way
741
+ let comparison_is_two_way = two_way(comparison_sides);
742
+
743
+ if(!is_two_way && comparison_is_two_way){
744
+ // if the comparison is two-way and this item is not, we know it should not be included
745
+ // because there is something higher priority
746
+ console.log(`Excluding ${readable_edit(edit,class_cache)} as there is a higher priority relation`)
747
+ edit.exclude=true;
748
+ include = false;
749
+ }else{
750
+ console.log(`Excluding ${readable_edit(comparison,class_cache)} as there is a higher priority relation`)
751
+ // we know the current item is higher priority, and so we ought to exclude the compared item for the rest of the loop
752
+ comparison.exclude=true;
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ return include;
759
+
760
+ })
761
+ }
762
+ }
763
+
764
+
765
+ action_delete_class(class_id:number){
766
+ // TBD
767
+ console.log('TBD, class deletion not yet implemented')
768
+ }
769
+
770
+
771
+
772
+
773
+
774
+ create_junction_table(sides:JunctionSides){
775
+
776
+ // adds new record to junction table
777
+ this.db.prepare(`
778
+ INSERT INTO system_junctionlist
779
+ (side_0_class_id, side_0_prop_id, side_1_class_id, side_1_prop_id)
780
+ VALUES (@side_0_class_id,@side_0_prop_id,@side_1_class_id,@side_1_prop_id)
781
+ `).run({
782
+ side_0_class_id:sides[0].class_id,
783
+ side_0_prop_id:sides[0].prop_id || null,
784
+ side_1_class_id:sides[1].class_id,
785
+ side_1_prop_id:sides[1].prop_id || null
786
+ });
787
+
788
+ //gets id of new record
789
+ let id=this.db.prepare<[],{id:number}>('SELECT id FROM system_junctionlist ORDER BY id DESC').get()?.id;
790
+ if(typeof id !== 'number') throw new Error('Something went wrong creating a new relationship');
791
+
792
+ // creates table
793
+ this.create_table('junction',id,[
794
+ `"${junction_col_name(sides[0].class_id,sides[0].prop_id)}" INTEGER`,
795
+ `"${junction_col_name(sides[1].class_id,sides[1].prop_id)}" INTEGER`
796
+ ]);
797
+
798
+ return id;
799
+ }
800
+
801
+
802
+ transfer_connections(source:{sides:JunctionSides,id:number},target:{sides:JunctionSides,id:number}){
803
+
804
+ let source_match_index=side_match(target.sides[0],source.sides[0])?0:1;
805
+
806
+ // flip the order if needed to maximally match the sides
807
+ let source_ordered=[
808
+ source.sides[source_match_index],
809
+ source.sides[Math.abs(source_match_index-1)]
810
+ ]
811
+
812
+ let source_col_names=[
813
+ junction_col_name(source_ordered[0].class_id,source_ordered[0].prop_id),
814
+ junction_col_name(source_ordered[1].class_id,source_ordered[1].prop_id)
815
+ ]
816
+
817
+ let target_col_names=[
818
+ junction_col_name(target.sides[0].class_id,target.sides[0].prop_id),
819
+ junction_col_name(target.sides[1].class_id,target.sides[1].prop_id)
820
+ ]
821
+
822
+ this.db.prepare(`
823
+ INSERT INTO junction_${target.id} (${target_col_names[0]},${target_col_names[1]})
824
+ SELECT ${source_col_names[0]}, ${source_col_names[1]} FROM junction_${source.id}`
825
+ ).run();
826
+ }
827
+
828
+ delete_junction_table(id:number){
829
+ this.db.prepare(`DELETE FROM system_junctionlist WHERE id = ${id}`).run();
830
+ this.db.prepare(`DROP TABLE junction_${id}`).run();
831
+ }
832
+
833
+ check_conditions({class_id,prop_id,property,class_data}:{class_id?:number,prop_id?:number,property?:Property,class_data:ClassData}){
834
+ /*
835
+ (some early ideas for how the conditions look;for now not gonna deal with filters or rules, just going to check max_values)
836
+ conditions={
837
+ filters:[
838
+
839
+ ],
840
+ rules:[
841
+
842
+ ]
843
+ }
844
+ */
845
+
846
+ // if(class_id!==undefined&&!class_data){
847
+ // class_data=this.retrieve_class_items({class_id});
848
+ // }
849
+ // if(prop_id!==undefined&&!property){
850
+ // // NOTE: change this in the future when properties moved to table
851
+ // property=class_data.metadata.properties.find(a=>a.id==prop_id);
852
+ // }
853
+
854
+ // if(property==undefined) throw new Error('Could not locate property')
855
+ // let prop_name='user_'+property.name;
856
+
857
+ // for(let item of class_data.items){
858
+ // let prop_values=item[prop_name];
859
+ // // check if they follow the conditions, and adjust if not.
860
+ // // for now I think just check max_values, and trim the values if not
861
+ // // I think (?) I can just read from the output of the cached class data,
862
+ // // and then use a prepare statement to modify data props on this table, and relation props on the corresponding junction table
863
+ // console.log(prop_name,prop_values);
864
+ // }
865
+
866
+ // after everything is done I should probably refresh the cache to get any changes to the items; maybe that can happen in the function where this is invoked though.
867
+
868
+ }
869
+
870
+ action_save(){
871
+ if(this.db.inTransaction) this.run.commit.run();
872
+ this.db.close();
873
+ }
874
+
875
+ action_create_item_in_root({type=null,value=''}:{type:string|null,value?:string}){
876
+ // this.db.prepare('INSERT INTO system_root VALUES (null)').run();
877
+ this.run.create_item.run({type,value});
878
+ let id=this.db.prepare<[],{id:number}>('SELECT id FROM system_root ORDER BY id DESC').get()?.id;
879
+ if(typeof id !== 'number') throw new Error('Something went wrong creating a new item');
880
+ return id;
881
+ }
882
+
883
+ action_delete_item_from_root(id:number){
884
+ this.db.prepare(`DELETE FROM system_root WHERE id = ${id}`).run();
885
+ }
886
+
887
+ action_set_root_item_value(id:number,value:string){
888
+ this.db.prepare(`UPDATE system_root set value = ? WHERE id = ?`).run(value,id);
889
+ }
890
+
891
+ action_add_row(class_id:number){
892
+ let class_data=this.class_cache.find(a=>a.id==class_id);
893
+ if(class_data == undefined) throw new Error('Cannot find class in class list.');
894
+ let class_name=class_data.name;
895
+
896
+ //first add new row to root and get id
897
+ const root_id=this.action_create_item_in_root({type:'class_'+class_id});
898
+
899
+
900
+ //get the last item in class table order and use it to get the order for the new item
901
+ const new_order=this.get_next_order(`[class_${class_name}]`);
902
+
903
+ this.db.prepare(`INSERT INTO [class_${class_name}] (system_id, system_order) VALUES (${root_id},${new_order})`).run();
904
+
905
+ return root_id;
906
+ }
907
+
908
+ get_next_order(table_name:string){
909
+ const last_ordered_item=this.db.prepare<[],{system_order:number}>(`SELECT system_order FROM ${table_name} ORDER BY system_order DESC`).get();
910
+ const new_order=last_ordered_item?last_ordered_item.system_order+1000:0;
911
+ return new_order;
912
+ }
913
+
914
+ action_make_relation(input_1:ItemRelationSide,input_2:ItemRelationSide){
915
+ // NOTE: changes to make to this in the future:
916
+ // - for input readability, allow class_name and prop_name as input options, assuming they’re enforced as unique, and use them to look up IDs
917
+ // - enforce max_values here
918
+
919
+
920
+ let column_names={
921
+ input_1:junction_col_name(input_1.class_id,input_1.prop_id),
922
+ input_2:junction_col_name(input_2.class_id,input_2.prop_id)
923
+ }
924
+ let junction_id=this.junction_cache.find(j=>full_relation_match(j.sides,[input_1,input_2]))?.id;
925
+
926
+ if(junction_id){
927
+ this.db.prepare(`
928
+ INSERT INTO junction_${junction_id}
929
+ ("${column_names.input_1}", "${column_names.input_2}")
930
+ VALUES (${input_1.item_id},${input_2.item_id})
931
+ `).run();
932
+ }else{
933
+ throw Error('Something went wrong - junction table for relationship not found')
934
+ }
935
+
936
+ // NOTE: should this trigger a refresh to items?
937
+ }
938
+
939
+
940
+ retrieve_class_items({class_id,class_name,class_data}:{class_id:number,class_name?:string,class_data?:ClassData}):ClassRow[]{
941
+ if(class_name==undefined || class_data == undefined){
942
+ class_data=this.class_cache.find(a=>a.id==class_id);
943
+ if(class_data == undefined) throw new Error('Cannot find class in class list.');
944
+ class_name=class_data.name;
945
+ };
946
+
947
+ const class_string=`[class_${class_name}]`;
948
+
949
+ // //joined+added at beginning of the query, built from relations
950
+ const cte_strings=[];
951
+
952
+ // //joined+added near the end of the query, built from relations
953
+ const cte_joins=[];
954
+
955
+ // //joined+added between SELECT and FROM, built from relations
956
+ const relation_selections=[];
957
+
958
+ let relation_properties=class_data.properties.filter(a=>a.type=='relation');
959
+
960
+ for (let prop of relation_properties){
961
+ const target_selects=[];
962
+ let property_junction_column_name=junction_col_name(class_id,prop.id);
963
+
964
+
965
+
966
+ if(prop.relation_targets.length>0){
967
+ for(let i = 0; i < prop.relation_targets.length; i++){
968
+
969
+ // find the side that does not match both the class and prop IDs
970
+ let target=prop.relation_targets[i]
971
+ if(target){
972
+ let target_junction_column_name=junction_col_name(target.class_id,target.prop_id);
973
+ let junction_id=target.junction_id;
974
+ let target_select=`SELECT "${property_junction_column_name}", json_object('class_id',${target.class_id},'id',"${target_junction_column_name}") AS target_data FROM junction_${junction_id}`
975
+ target_selects.push(target_select);
976
+ }else{
977
+ throw Error('Something went wrong trying to retrieve relationship data')
978
+ }
979
+
980
+ }
981
+
982
+ // uses built-in aggregate json function instead of group_concat craziness
983
+ const cte=`[${prop.id}_cte] AS (
984
+ SELECT "${property_junction_column_name}", json_group_array( json(target_data) ) AS [user_${prop.name}]
985
+ FROM
986
+ (
987
+ ${target_selects.join(`
988
+ UNION
989
+ `)}
990
+ )
991
+ GROUP BY "${property_junction_column_name}"
992
+ )`
993
+
994
+ cte_strings.push(cte);
995
+ relation_selections.push(`[${prop.id}_cte].[user_${prop.name}]`);
996
+ cte_joins.push(`LEFT JOIN [${prop.id}_cte] ON [${prop.id}_cte]."${property_junction_column_name}" = ${class_string}.system_id`)
997
+ }else{
998
+ relation_selections.push(`'[]' AS [user_${prop.name}]`);
999
+ }
1000
+
1001
+
1002
+ }
1003
+
1004
+ let orderby=`ORDER BY ${class_string}.system_order`;
1005
+
1006
+ let comma_break=`,
1007
+ `
1008
+
1009
+ let query=`
1010
+ ${cte_strings.length>0?"WITH "+cte_strings.join(comma_break):''}
1011
+ SELECT [class_${class_name}].* ${relation_selections.length>0?', '+relation_selections.join(`, `):''}
1012
+ FROM [class_${class_name}]
1013
+ ${cte_joins.join(' ')}
1014
+ ${orderby}`;
1015
+
1016
+ // possibly elaborate this any type a little more in the future, e.g. a CellValue or SQLCellValue type that expects some wildcards
1017
+ let items=this.db.prepare<[],ClassRow>(query).all();
1018
+
1019
+ let stringified_properties=class_data.properties.filter(a=>a.type=='relation'||can_have_multiple_values(a.max_values));
1020
+ items.map((row)=>{
1021
+ if(row && typeof row == 'object'){
1022
+ for (let prop of stringified_properties){
1023
+ let prop_sql_name='user_'+prop.name;
1024
+ if(prop_sql_name in row){
1025
+ row[prop_sql_name]=JSON.parse(row[prop_sql_name]);
1026
+ }
1027
+ }
1028
+ }
1029
+ });
1030
+ return items;
1031
+ }
1032
+
1033
+ retrieve_all_classes():ClassData[]{
1034
+ const classes_data=this.run.get_all_classes.all();
1035
+ return classes_data.map(({id,name,metadata})=>{
1036
+ let existing_items=this.item_cache.find((itemlist)=>itemlist.class_id==id);
1037
+
1038
+ let properties_sql=this.db.prepare<[],{id:number,type:PropertyType,data_type:'string'| null,max_values:MaxValues,name:string,metadata:string}>(`SELECT * FROM class_${id}_properties`).all() || [];
1039
+
1040
+ let properties=properties_sql.map((sql_prop)=>this.parse_sql_prop(id,sql_prop));
1041
+
1042
+ return {
1043
+ id,
1044
+ name,
1045
+ items:existing_items?.items ?? [],
1046
+ properties,
1047
+ metadata:JSON.parse(metadata)
1048
+ };
1049
+ })
1050
+ }
1051
+
1052
+ parse_sql_prop(
1053
+ class_id:number,
1054
+ sql_prop:{
1055
+ id:number,
1056
+ type:PropertyType,
1057
+ data_type:'string'| null,
1058
+ max_values:MaxValues,
1059
+ name:string,
1060
+ metadata:string
1061
+ }):(DataProperty | RelationProperty){
1062
+
1063
+ if(sql_prop.type=='data'&&defined(sql_prop.data_type)){
1064
+ return {
1065
+ type:'data',
1066
+ id:sql_prop.id,
1067
+ name:sql_prop.name,
1068
+ max_values:sql_prop.max_values,
1069
+ data_type:sql_prop.data_type
1070
+ };
1071
+ }else if(sql_prop.type=='relation'){
1072
+ let associated_junctions=this.run.get_junctions_matching_property.all({class_id:class_id,prop_id:sql_prop.id}) || [];
1073
+ let relation_targets=associated_junctions.map((j)=>{
1074
+ let sides:JunctionSides=JSON.parse(j.sides);
1075
+ // find the side that does not match both the class and prop IDs
1076
+ let target=sides.find(a=>!(a.class_id==class_id&&a.prop_id==sql_prop.id));
1077
+ if(!target) throw Error('Something went wrong locating target of relationship')
1078
+ return {...target,junction_id:j.id};
1079
+ })
1080
+ return {
1081
+ type:'relation',
1082
+ id:sql_prop.id,
1083
+ name:sql_prop.name,
1084
+ max_values:sql_prop.max_values,
1085
+ relation_targets
1086
+ };
1087
+ }else{
1088
+ throw Error('property type does not match known types')
1089
+ }
1090
+ }
1091
+
1092
+ retrieve_windows(){
1093
+ let windows=this.run.get_windows.all();
1094
+ windows.map(a=>a.metadata=JSON.parse(a.metadata));
1095
+ return windows;
1096
+ }
1097
+
1098
+ retrieve_workspace_contents(id:number){
1099
+ // get the workspace table
1100
+ let blocks=this.db.prepare<[],SQLWorkspaceBlockRow>(`SELECT * FROM workspace_${id}`).all();
1101
+ let blocks_parsed:WorkspaceBlock[]=blocks.map(a=>({
1102
+ ...a,
1103
+ metadata:JSON.parse(a.metadata)
1104
+ }))
1105
+
1106
+ // for(let block of blocks) block.metadata=JSON.parse(block.metadata);
1107
+ // get any relevant root items
1108
+ let items=this.db.prepare(`SELECT system_root.*
1109
+ FROM system_root
1110
+ LEFT JOIN workspace_${id}
1111
+ ON system_root.id = workspace_${id}.thing_id
1112
+ WHERE workspace_${id}.type = 'item';
1113
+ `).all();
1114
+ // get any relevant classes (going to hold off from this for now)
1115
+
1116
+ return {
1117
+ blocks_parsed,
1118
+ items
1119
+ }
1120
+ }
1121
+
1122
+ action_config_window({type,open,metadata={pos:[null,null], size:[1000,700]},id}:{
1123
+ type:ApplicationWindow["type"],
1124
+ open:ApplicationWindow["open"],
1125
+ metadata:ApplicationWindow["metadata"],
1126
+ id:number
1127
+ }){
1128
+ if(id!==undefined){
1129
+ this.run.update_window.run({
1130
+ id,
1131
+ open,
1132
+ type,
1133
+ meta:JSON.stringify(metadata)
1134
+ })
1135
+ }else{
1136
+
1137
+ let id=this.create_workspace(open,metadata)
1138
+
1139
+ return id;
1140
+ }
1141
+
1142
+ }
1143
+
1144
+
1145
+ create_workspace(open:ApplicationWindow["open"],metadata:ApplicationWindow["metadata"]){
1146
+
1147
+ this.run.create_window.run({
1148
+ type:'workspace',
1149
+ open,
1150
+ meta:JSON.stringify(metadata)
1151
+ })
1152
+
1153
+ let id=this.get_latest_table_row_id('system_windows')
1154
+ if(!id) throw Error('Something went wrong creating the window.');
1155
+
1156
+ this.create_table('workspace',id,[
1157
+ 'block_id INTEGER NOT NULL PRIMARY KEY',
1158
+ 'type TEXT',
1159
+ 'metadata TEXT',
1160
+ 'thing_id INTEGER'
1161
+ ]);
1162
+
1163
+ return id;
1164
+ }
1165
+
1166
+ action_create_workspace_block({workspace_id,thing_type,block_metadata,thing_id}:{
1167
+ workspace_id:ApplicationWindow["id"],
1168
+ thing_type:WorkspaceBlock["thing_type"],
1169
+ block_metadata:WorkspaceBlock["metadata"],
1170
+ thing_id:WorkspaceBlock["thing_id"]
1171
+ }){
1172
+ // should return block id
1173
+ this.db.prepare<{type:string,metadata:string,thing_id:number}>(`INSERT INTO workspace_${workspace_id}(type,metadata,thing_id) VALUES (@type,@metadata,@thing_id)`).run({
1174
+ type:thing_type,
1175
+ metadata:JSON.stringify(block_metadata),
1176
+ thing_id
1177
+ });
1178
+ let block_id=this.db.prepare<[],{block_id:number}>(`SELECT block_id FROM workspace_${workspace_id} ORDER BY block_id DESC`).get()?.block_id;
1179
+ if(block_id==undefined) throw Error("Problem adding block to workspace");
1180
+ return block_id;
1181
+
1182
+ }
1183
+
1184
+ action_remove_workspace_block({workspace_id,block_id}:{workspace_id:number,block_id:number}){
1185
+ this.db.prepare(`DELETE FROM workspace_${workspace_id} WHERE block_id = ${block_id}`).run();
1186
+ };
1187
+
1188
+ action_create_and_add_to_workspace({
1189
+ workspace_id,
1190
+ thing_type,
1191
+ block_metadata,
1192
+ thing_data
1193
+ }:{
1194
+ workspace_id:number,
1195
+ thing_type:WorkspaceBlock["thing_type"],
1196
+ block_metadata:WorkspaceBlock["metadata"],
1197
+ thing_data:any // NOTE: fix this in the future when I define class and standalone item types
1198
+ }){
1199
+ let thing_id;
1200
+ // thing creation
1201
+ switch(thing_type){
1202
+ case 'item':
1203
+ let {
1204
+ value:item_value,
1205
+ type:item_type
1206
+ } = thing_data;
1207
+ thing_id=this.action_create_item_in_root({type:item_type,value:item_value});
1208
+ break;
1209
+ // add cases for class and anything else in the future
1210
+ }
1211
+
1212
+ if(!thing_id) throw Error('Something went wrong saving an item from a workspace');
1213
+
1214
+ let block_id=this.action_create_workspace_block({
1215
+ workspace_id,
1216
+ thing_type,
1217
+ block_metadata,
1218
+ thing_id
1219
+ })
1220
+
1221
+ return {
1222
+ thing_id,
1223
+ block_id
1224
+ }
1225
+ // should return the block id and item id
1226
+ }
1227
+
1228
+ action_remove_from_workspace_and_delete(workspace_id:number,block_id:number,thing_type:WorkspaceBlock["thing_type"],thing_id:number){
1229
+ this.action_remove_workspace_block({workspace_id,block_id});
1230
+ switch(thing_type){
1231
+ case 'item':
1232
+ this.action_delete_item_from_root(thing_id);
1233
+ break;
1234
+ }
1235
+ }
1236
+
1237
+ }
1238
+
1239
+
1240
+
1241
+
1242
+ // // match both classes
1243
+ // // match at least one prop
1244
+ // let a0_match_i=b.findIndex(side=>a[0].class_id==side.class_id);
1245
+ // let a1_match_i=b.findIndex(side=>a[1].class_id==side.class_id);
1246
+ // if(a0_match_i>=0&&a1_match_i>=0&&a0_match_i!==a1_match_i){
1247
+ // return b[a0_match_i].prop_id==a[0].prop_id||
1248
+ // b[a1_match_i].prop_id==a[1].prop_id
1249
+ // }else{
1250
+ // return false;
1251
+ // }