goby-database 1.0.9 → 2.0.10

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/sandbox.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { readable_junctionlist } from './utils.js';
2
+
3
+ console.log('--------------------------------------------------------')
4
+ console.log('SANDBOX: running test...\n')
5
+
6
+
7
+ import Project from './index.js';
8
+ const project=new Project(':memory:');
9
+
10
+ console.log('--------------------------------------------------------')
11
+ console.log('SANDBOX: setting up book-author-script schema\n')
12
+ project.action_edit_class_schema({
13
+ class_edits:[
14
+ {type:'create',class_name:'author'},
15
+ {type:'create',class_name:'book'},
16
+ {type:'create',class_name:'script'}
17
+ ],
18
+ property_edits:[
19
+ {type:'create',class_name:'author',prop_name:'age',config:{type:'data',data_type:'number',max_values:1}},
20
+ {type:'create',class_name:'author',prop_name:'works',config:{type:'relation',max_values:null}},
21
+ {type:'create',class_name:'author',prop_name:'books read',config:{type:'relation',max_values:null}},
22
+ {type:'create',class_name:'book',prop_name:'author',config:{type:'relation',max_values:1}}
23
+ ],
24
+ relationship_edits:[
25
+ { type:'create', sides:[{class_name:'author',prop_name:'works'}, {class_name:'book',prop_name:'author'}] },
26
+ { type:'create', sides:[{class_name:'author',prop_name:'works'}, {class_name:'script'}] },
27
+ { type:'create',sides:[{class_name:'author',prop_name:'books read'},{class_name:'book'}]}
28
+ ]
29
+ })
30
+ console.log(project.junction_cache.map(a=>a.sides));
31
+
32
+ console.log('--------------------------------------------------------')
33
+ console.log('SANDBOX: adding items to classes\n')
34
+ project.action_add_row(1);
35
+ project.action_add_row(2);
36
+ project.action_add_row(2);
37
+ project.action_add_row(3);
38
+
39
+ console.log('--------------------------------------------------------')
40
+ console.log('SANDBOX: making connections between items in classes\n')
41
+ project.action_make_relation({
42
+ class_id:1,
43
+ prop_id:3,
44
+ item_id:1
45
+ },{
46
+ class_id:2,
47
+ prop_id:2,
48
+ item_id:2
49
+ })
50
+ project.action_make_relation({
51
+ class_id:1,
52
+ prop_id:3,
53
+ item_id:1
54
+ },{
55
+ class_id:2,
56
+ prop_id:2,
57
+ item_id:3
58
+ })
59
+
60
+ project.action_make_relation({
61
+ class_id:1,
62
+ prop_id:3,
63
+ item_id:1
64
+ },{
65
+ class_id:3,
66
+ item_id:4
67
+ })
68
+ project.action_make_relation({
69
+ class_id:1,
70
+ prop_id:4,
71
+ item_id:1
72
+ },{
73
+ class_id:2,
74
+ item_id:2
75
+ })
76
+
77
+ project.refresh_caches(['classlist','items','junctions']);
78
+
79
+
80
+ console.log('--------------------------------------------------------')
81
+ console.log('SANDBOX: deleting author property in books\n')
82
+
83
+ project.action_edit_class_schema(
84
+ {
85
+ property_edits:[
86
+ {
87
+ type:'delete',
88
+ class_id:2,
89
+ prop_id:2
90
+ },
91
+ {
92
+ type:'create',
93
+ class_id:2,
94
+ prop_name:'author2',
95
+ config:{
96
+ type:'relation',
97
+ max_values:1
98
+ }
99
+ }
100
+ ],
101
+ relationship_edits:[
102
+ {type:'create',sides:[{class_id:1,prop_id:3},{class_id:2,prop_name:'author2'}]}
103
+ ]
104
+ }
105
+ )
106
+
107
+ console.log(readable_junctionlist(project.junction_cache,project.class_cache));
package/src/types.ts ADDED
@@ -0,0 +1,218 @@
1
+ export type SQLTableType = 'system' | 'class' | 'properties' | 'junction' | 'workspace';
2
+
3
+ export type SQLClassListRow = {
4
+ id: number;
5
+ name: string;
6
+ metadata: string; // JSON string
7
+ };
8
+
9
+ export type SQLJunctonListRow = {
10
+ id: number;
11
+ sides: string; //JSON string
12
+ metadata: string; // JSON string
13
+ };
14
+
15
+ export type RelationshipSideBase = {
16
+ class_id?:number;
17
+ prop_id?:number;
18
+ class_name?:string;
19
+ prop_name?:string;
20
+ }
21
+
22
+ export type RelationshipSide = RelationshipSideBase & {
23
+ class_id:number;
24
+ };
25
+
26
+ export type RelationTarget = {
27
+ class_id:number;
28
+ prop_id?:number | null;
29
+ junction_id:number;
30
+ }
31
+
32
+
33
+ export type ItemRelationSide = RelationshipSide & {
34
+ item_id:number;
35
+ }
36
+
37
+
38
+ type RelationCreate = {
39
+ type:'create',
40
+ sides:[
41
+ RelationshipSideBase,
42
+ RelationshipSideBase
43
+ ]
44
+ }
45
+
46
+ type RelationDelete = {
47
+ type:'delete',
48
+ id:number
49
+ }
50
+
51
+ type RelationTransfer = {
52
+ type:'transfer',
53
+ id:number,
54
+ sides:[
55
+ RelationshipSide,
56
+ RelationshipSide
57
+ ],
58
+ // ; to allow conversion from two-way to one-way relation (and one way to two-way?)
59
+ new_sides:[
60
+ RelationshipSideBase,
61
+ RelationshipSideBase
62
+ ]
63
+ }
64
+
65
+
66
+ export type RelationEdit = RelationCreate | RelationDelete | RelationTransfer;
67
+
68
+ export type RelationEditValidSides = (RelationCreate & {sides:JunctionSides})| (RelationTransfer & {sides:JunctionSides, new_sides:JunctionSides}) | RelationDelete;
69
+
70
+ type PropertyCreate ={
71
+ type:'create',
72
+ prop_name:string
73
+ class_id?:number,
74
+ class_name?:string,
75
+ config:PropertyDefinition
76
+ }
77
+
78
+ type PropertyDelete = {
79
+ type:'delete',
80
+ class_id:number
81
+ prop_id:number
82
+ }
83
+
84
+ type PropertyModify ={
85
+ type:'modify',
86
+ prop_id:number,
87
+ class_id:number
88
+ config:PropertyDefinition
89
+ }
90
+
91
+ export type PropertyEdit = PropertyCreate | PropertyDelete | PropertyModify
92
+
93
+ type ClassCreate = {
94
+ type:'create',
95
+ class_name:string
96
+ }
97
+
98
+ type ClassDelete = {
99
+ type:'delete',
100
+ class_id:number
101
+ }
102
+
103
+ type ClassModify = {
104
+ type:'modify_attribute';
105
+ class_id:number;
106
+ attribute:{
107
+ // NOTE: for future defining, this should be one of a list of possible values
108
+ name:string;
109
+ // ... and this should be conditioned by the name
110
+ value:any;
111
+ }
112
+ }
113
+
114
+ export type ClassEdit = ClassCreate | ClassDelete | ClassModify;
115
+
116
+ export type ClassMetadata ={
117
+ style:{
118
+ color?:string
119
+ }
120
+ }
121
+
122
+ export type PropertyType = 'data' | 'relation';
123
+
124
+ export type BinaryBoolean = 0 | 1;
125
+
126
+ // null if no max value
127
+ export type MaxValues = number | null;
128
+
129
+ export type BaseProperty={
130
+ id:number,
131
+ name:string,
132
+ type:PropertyType,
133
+ max_values:MaxValues
134
+ }
135
+
136
+
137
+ export type RelationDefinition = {
138
+ type:'relation';
139
+ relation_targets:RelationTarget[];
140
+ data_type?:never;
141
+ }
142
+
143
+ export type RelationProperty = BaseProperty & RelationDefinition;
144
+
145
+ export type DataType = 'string' |'resource' | 'number';
146
+
147
+ export type DataDefinition = {
148
+ type:'data';
149
+ data_type:DataType;
150
+ relation_targets?:never;
151
+ }
152
+
153
+ export type DataProperty = BaseProperty & DataDefinition;
154
+
155
+ export type Property = RelationProperty | DataProperty;
156
+
157
+ export type PropertyDefinition = {max_values:MaxValues} & ({type:'relation'} | DataDefinition);
158
+
159
+
160
+ export type ClassRow={ [key: string]: any };
161
+
162
+ export type ClassData ={
163
+ id:number;
164
+ name:string;
165
+ metadata:ClassMetadata;
166
+ items:ClassRow[];
167
+ properties:Property[];
168
+ };
169
+
170
+ export type ClassList =ClassData[];
171
+
172
+
173
+
174
+ export type JunctionSides =[RelationshipSide,RelationshipSide];
175
+
176
+ export type JunctionTable ={
177
+ id:number,
178
+ sides:JunctionSides,
179
+ metadata:{}
180
+ }
181
+
182
+
183
+ export type JunctionList =JunctionTable[];
184
+
185
+
186
+ export type SQLApplicationWindow ={
187
+ id:number;
188
+ type:string;
189
+ open:BinaryBoolean;
190
+ metadata:string; // JSON string
191
+ }
192
+ export type ApplicationWindow ={
193
+ id:number;
194
+ type:string;
195
+ open:BinaryBoolean;
196
+ metadata:{};
197
+ }
198
+
199
+ // maybe elaborate this in the future
200
+ export type ThingType = 'item' | 'class';
201
+
202
+ export type BaseWorkspaceBlock = {
203
+ /** this is the ID of the display element in the workspace, generated by the workspace table */
204
+ block_id:number;
205
+
206
+ thing_type:ThingType;
207
+
208
+ /** this is the ID of the class, item, or other “thing” represented in the data */
209
+ thing_id:number;
210
+ }
211
+
212
+ export type SQLWorkspaceBlockRow = BaseWorkspaceBlock & {
213
+ metadata:string; // json string
214
+ }
215
+
216
+ export type WorkspaceBlock = BaseWorkspaceBlock & {
217
+ metadata:{};
218
+ }
@@ -0,0 +1,117 @@
1
+ import { JunctionSides } from "./types";
2
+ import { partial_relation_match } from "./utils.js";
3
+
4
+ testRelationMatching();
5
+
6
+ function testRelationMatching() {
7
+ const test1_old: JunctionSides = [
8
+ { class_id: 1, prop_id: 100 },
9
+ { class_id: 2, prop_id: 200 },
10
+ ];
11
+
12
+ const test1_new: JunctionSides = [
13
+ { class_id: 1, prop_id: 100 },
14
+ { class_id: 2, prop_id: 300 },
15
+ ];
16
+
17
+ const test2_old: JunctionSides = [
18
+ { class_id: 1, prop_id: 100 },
19
+ { class_id: 2, prop_id: 200 },
20
+ ];
21
+ const test2_new: JunctionSides = [
22
+ { class_id: 1, prop_id: 300 },
23
+ { class_id: 2, prop_id: 400 },
24
+ ];
25
+
26
+ // Case 3: Non-matching classes
27
+ const test3_old: JunctionSides = [
28
+ { class_id: 1, prop_id: 100 },
29
+ { class_id: 2, prop_id: 200 },
30
+ ];
31
+ const test3_new: JunctionSides = [
32
+ { class_id: 3, prop_id: 100 },
33
+ { class_id: 4, prop_id: 200 },
34
+ ];
35
+
36
+ const test4a_old: JunctionSides = [
37
+ { class_id: 1, prop_id: 100 },
38
+ { class_id: 1, prop_id: 200 },
39
+ ];
40
+ const test4a_new: JunctionSides = [
41
+ { class_id: 1, prop_id: 100 },
42
+ { class_id: 1, prop_id: 300 },
43
+ ];
44
+
45
+ // Case 4b: Same class on both sides with no matching properties
46
+ const test4b_old: JunctionSides = [
47
+ { class_id: 1, prop_id: 100 },
48
+ { class_id: 1, prop_id: 200 },
49
+ ];
50
+ const test4b_new: JunctionSides = [
51
+ { class_id: 1, prop_id: 300 },
52
+ { class_id: 1, prop_id: 400 },
53
+ ];
54
+
55
+ // Case 5: Reversed order matching
56
+ const test5_old: JunctionSides = [
57
+ { class_id: 1, prop_id: 100 },
58
+ { class_id: 2, prop_id: 200 },
59
+ ];
60
+ const test5_new: JunctionSides = [
61
+ { class_id: 2, prop_id: 200 },
62
+ { class_id: 1, prop_id: 300 },
63
+ ];
64
+
65
+ // Case 6: Undefined property IDs
66
+ const test6_old: JunctionSides = [
67
+ { class_id: 1 },
68
+ { class_id: 2, prop_id: 200 },
69
+ ];
70
+ const test6_new: JunctionSides = [
71
+ { class_id: 1, prop_id: 100 },
72
+ { class_id: 2 },
73
+ ];
74
+
75
+ // Case 7: Undefined property IDs on same side
76
+ const test7_old: JunctionSides = [
77
+ { class_id: 1 },
78
+ { class_id: 2, prop_id: 200 },
79
+ ];
80
+ const test7_new: JunctionSides = [
81
+ { class_id: 1 },
82
+ { class_id: 2, prop_id: 100 },
83
+ ];
84
+
85
+ console.log(
86
+ "same class, at least one prop (expect true)",
87
+ partial_relation_match(test1_new, test1_old)
88
+ );
89
+ console.log(
90
+ "same class, no props (expect false)",
91
+ partial_relation_match(test2_old, test2_new)
92
+ );
93
+ console.log(
94
+ "different classes (expect false)",
95
+ partial_relation_match(test3_old, test3_new)
96
+ );
97
+ console.log(
98
+ "only one class, matching prop (expect true)",
99
+ partial_relation_match(test4a_new, test4a_old)
100
+ );
101
+ console.log(
102
+ "only one class, no props (expect false)",
103
+ partial_relation_match(test4b_old, test4b_new)
104
+ );
105
+ console.log(
106
+ "reversed class order, prop matching (expect true)",
107
+ partial_relation_match(test5_new, test5_old)
108
+ );
109
+ console.log(
110
+ "undefined prop (expect false)",
111
+ partial_relation_match(test6_old, test6_new)
112
+ );
113
+ console.log(
114
+ "undefined prop 2 (expect false)",
115
+ partial_relation_match(test7_old, test7_new)
116
+ );
117
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,133 @@
1
+ import { JunctionSides, RelationshipSide,MaxValues,RelationshipSideBase, RelationEdit, RelationEditValidSides,ClassData,JunctionList } from "./types";
2
+
3
+
4
+ export function defined<T>(v:T):v is NonNullable<T>{
5
+ return v!==undefined && v!==null;
6
+ }
7
+
8
+
9
+ // given the type above,
10
+ // check if two relations share class ids on both sides
11
+ // and share a property id on at least one side
12
+ // note: it’s possible for both sides of a relation to have the same class id
13
+
14
+ export function partial_relation_match(old_relation:JunctionSides,new_relation:JunctionSides):boolean{
15
+
16
+ // find class id matches in old for each side of new relation
17
+ let new_side_matches_in_old=[
18
+ [0,1].filter((i)=> old_relation[i].class_id==new_relation[0].class_id),
19
+ [0,1].filter((i)=> old_relation[i].class_id==new_relation[1].class_id)
20
+ ];
21
+
22
+ // for each match in the first side of the new relation
23
+ for(let match_index of new_side_matches_in_old[0]){
24
+
25
+ // converts 0 to 1 and 1 to 0, to get opposite side
26
+ let opposite_index=Math.abs(match_index-1);
27
+
28
+ // if this check passes, the relations share class IDs on both sides
29
+ if(new_side_matches_in_old[1].includes(opposite_index)){
30
+ // class match A
31
+ let match_a_new=new_relation[0];
32
+ let match_a_old=old_relation[match_index];
33
+
34
+ // class match B
35
+ let match_b_new=new_relation[1];
36
+ let match_b_old=old_relation[opposite_index];;
37
+
38
+ // should return true if the properties match on at least one side
39
+ if(properties_exist_and_match(match_a_new,match_a_old) || properties_exist_and_match(match_b_new,match_b_old)){
40
+ return true;
41
+ }
42
+ }
43
+
44
+ }
45
+
46
+ // if no matches found, return false found,
47
+ return false;
48
+
49
+ }
50
+
51
+ function properties_exist_and_match(a:RelationshipSide,b:RelationshipSide){
52
+ return defined(a.prop_id) && defined(b.prop_id) && a.prop_id == b.prop_id;
53
+ }
54
+
55
+ function properties_match(a:RelationshipSide,b:RelationshipSide){
56
+ if(!defined(a.prop_id) && !defined(b.prop_id)) return true;
57
+ else return a.prop_id == b.prop_id;
58
+
59
+ // return defined(a.prop_id) && defined(b.prop_id) && a.prop_id == b.prop_id;
60
+ }
61
+
62
+ export function side_match(x:RelationshipSide,y:RelationshipSide){
63
+ return x.class_id==y.class_id&&properties_match(x,y);
64
+ };
65
+
66
+ export function full_relation_match(a:JunctionSides,b:JunctionSides):boolean{
67
+ return (side_match(a[0],b[0])&&side_match(a[1],b[1])) ||
68
+ (side_match(a[0],b[1])&&side_match(a[1],b[0]));
69
+ }
70
+
71
+ export function valid_sides(sides:[RelationshipSideBase,RelationshipSideBase]):sides is [RelationshipSide,RelationshipSide]{
72
+ return defined(sides[0].class_id) && defined(sides[1].class_id)
73
+ }
74
+
75
+ export function two_way(sides:JunctionSides){
76
+ return defined(sides[0].prop_id) && defined(sides[1].prop_id);
77
+ }
78
+
79
+ export function edit_has_valid_sides(edit:RelationEdit):edit is RelationEditValidSides{
80
+ switch(edit.type){
81
+ case 'create':
82
+ if(valid_sides(edit.sides)) return true;
83
+ break;
84
+ case 'transfer':
85
+ if(valid_sides(edit.sides)&&valid_sides(edit.new_sides)) return true;
86
+ break;
87
+ case 'delete':{
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+
94
+
95
+
96
+ export function can_have_multiple_values(max_values:MaxValues){
97
+ return max_values==null || max_values>1;
98
+ }
99
+
100
+ export function junction_col_name(class_id:number,prop_id:number | undefined | null):string{
101
+ let prop_str=defined(prop_id)?`_prop_${prop_id}`:``
102
+ return `class_${class_id}${prop_str}`;
103
+ }
104
+
105
+
106
+ export function readable_side(side:RelationshipSide,classlist:ClassData[]){
107
+ let matching_class=classlist.find(c=>c.id==side.class_id);
108
+ let matching_prop=side.prop_id?(matching_class?.properties?.find(p=>p.id==side.prop_id)?.name || ''):'';
109
+
110
+ matching_prop=matching_prop?`.[${matching_prop}]`:'';
111
+
112
+
113
+ return `${matching_class?.name || ''}${matching_prop}`;
114
+ }
115
+
116
+ function readable_sides(sides:JunctionSides,classlist:ClassData[]){
117
+ return `${readable_side(sides[0],classlist)} <-> ${readable_side(sides[1],classlist)}`
118
+ }
119
+
120
+
121
+ export function readable_edit(edit:RelationEditValidSides,classlist:ClassData[]){
122
+ if(edit.type=='delete') return `deletion of relation ${edit.id}`;
123
+ else if (edit.type=='transfer') return `transfer of (${readable_sides(edit.sides,classlist)}) to (${readable_sides(edit.new_sides,classlist)})`;
124
+ else if (edit.type=='create') return `creation of (${readable_sides(edit.sides,classlist)})`;
125
+ }
126
+
127
+
128
+ export function readable_junctionlist(relationships:JunctionList,classlist:ClassData[]){
129
+
130
+ return relationships.map((r)=>{
131
+ return readable_sides(r.sides,classlist)
132
+ })
133
+ }