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