goby-database 2.2.28 → 2.2.30

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.d.ts CHANGED
@@ -70,12 +70,31 @@ export default class Project {
70
70
  delete_item_from_root(id: number): void;
71
71
  action_set_root_item_value(id: number, value: string): void;
72
72
  lookup_class(class_id: number): ClassData;
73
- action_add_row(class_id: number): number;
73
+ /**
74
+ * Creates a new item and adds it to the class you indicate
75
+ * @param class_id - class you want to add new item to
76
+ * @param property_values - any properties you want to fill in as you create this item (data props only for now)
77
+ * @returns - id of new item
78
+ */
79
+ action_add_row(class_id: number, property_values?: {
80
+ property_id: number;
81
+ value: any;
82
+ }[]): number;
74
83
  get_next_order(table_name: string): number;
75
- action_set_property_values(class_id: number, item_id: number, changes: {
84
+ /**
85
+ * Sets 1 or more data property values for a given class item.
86
+ * @param class_id - class of item
87
+ * @param item_id - id of item
88
+ * @param changes - array of data properties to be set
89
+ */
90
+ action_edit_item_data(class_id: number, item_id: number, changes: {
76
91
  property_id: number;
77
92
  value: any;
78
93
  }[]): void;
94
+ /**
95
+ * Adds/removes relations between items/item properties
96
+ * @param relations - list of pairs of items for which relations should be added or removed between specified properties
97
+ */
79
98
  action_edit_relations(relations: {
80
99
  change: 'add' | 'remove';
81
100
  sides: [input_1: ItemRelationSide, input_2: ItemRelationSide];
package/dist/index.js CHANGED
@@ -1,8 +1,5 @@
1
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 integer_data_types = ['boolean'];
5
- const real_data_types = ['number'];
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, text_data_types, integer_data_types, real_data_types, validate_data_value } from './utils.js';
6
3
  export default class Project {
7
4
  constructor(source) {
8
5
  this.class_cache = [];
@@ -17,7 +14,8 @@ export default class Project {
17
14
  else {
18
15
  console.log('opened goby database');
19
16
  }
20
- //prepared statements with arguments so my code isn't as verbose elsewhere
17
+ // prepared statements with arguments so my code isn't as verbose elsewhere
18
+ // obviously can only do this for actions on determinate columns
21
19
  this.run = {
22
20
  begin: this.db.prepare('BEGIN IMMEDIATE'),
23
21
  commit: this.db.prepare('COMMIT'),
@@ -689,14 +687,42 @@ export default class Project {
689
687
  throw new Error('Cannot find class in class list.');
690
688
  return class_data;
691
689
  }
692
- action_add_row(class_id) {
690
+ /**
691
+ * Creates a new item and adds it to the class you indicate
692
+ * @param class_id - class you want to add new item to
693
+ * @param property_values - any properties you want to fill in as you create this item (data props only for now)
694
+ * @returns - id of new item
695
+ */
696
+ action_add_row(class_id, property_values = []) {
693
697
  const class_data = this.lookup_class(class_id);
694
698
  let class_name = class_data.name;
695
699
  //first add new row to root and get id
696
700
  const root_id = this.create_item_in_root({ type: 'class_' + class_id });
701
+ // NOTE: I will need a second handler in the future if I want to support relation props here
702
+ // (probably utilizing action_edit_relations)
703
+ const data_property_sql = property_values.reduce((obj, current) => {
704
+ const corresponding_prop = class_data.properties.find((prop) => {
705
+ return prop.id == current.property_id;
706
+ });
707
+ if ((corresponding_prop === null || corresponding_prop === void 0 ? void 0 : corresponding_prop.type) == 'data') {
708
+ const validated = validate_data_value(current.value, corresponding_prop.data_type, corresponding_prop.max_values);
709
+ if (validated.valid) {
710
+ const v = text_data_types.includes(corresponding_prop.data_type) ? `'${validated.output}'` : validated.output;
711
+ obj.columns += `, [user_${corresponding_prop.name}]`;
712
+ obj.values += `, ${v}`;
713
+ }
714
+ else {
715
+ console.log(`Did not modify ${corresponding_prop.name} for item: ${validated.message}`);
716
+ }
717
+ }
718
+ return obj;
719
+ }, {
720
+ columns: '',
721
+ values: ''
722
+ });
697
723
  //get the last item in class table order and use it to get the order for the new item
698
724
  const new_order = this.get_next_order(`[class_${class_name}]`);
699
- this.db.prepare(`INSERT INTO [class_${class_name}] (system_id, system_order) VALUES (${root_id},${new_order})`).run();
725
+ this.db.prepare(`INSERT INTO [class_${class_name}] (system_id, system_order${data_property_sql.columns}) VALUES (${root_id},${new_order}${data_property_sql.values})`).run();
700
726
  return root_id;
701
727
  }
702
728
  get_next_order(table_name) {
@@ -704,15 +730,21 @@ export default class Project {
704
730
  const new_order = last_ordered_item ? last_ordered_item.system_order + 1000 : 0;
705
731
  return new_order;
706
732
  }
707
- // NOTE: seems like there should be a way to pair down "value:any" in params, maybe at least make it one of a few value types
708
- action_set_property_values(class_id, item_id, changes) {
733
+ /**
734
+ * Sets 1 or more data property values for a given class item.
735
+ * @param class_id - class of item
736
+ * @param item_id - id of item
737
+ * @param changes - array of data properties to be set
738
+ */
739
+ action_edit_item_data(class_id, item_id, changes) {
740
+ // NOTE: seems like there should be a way to pair down "value:any" in params, maybe at least make it one of a few value types
709
741
  const class_data = this.lookup_class(class_id);
710
742
  const sql_column_inserts = [];
711
743
  for (let change of changes) {
712
744
  const prop_data = class_data.properties.find((p) => p.id == change.property_id);
713
745
  if (prop_data && prop_data.type == 'data') {
714
746
  // const data_type=prop_data.data_type;
715
- const cell_value = validate(change.value, prop_data.data_type, prop_data.max_values);
747
+ const cell_value = validate_data_value(change.value, prop_data.data_type, prop_data.max_values);
716
748
  if (cell_value.valid) {
717
749
  sql_column_inserts.push({
718
750
  column_name: `[user_${prop_data.name}]`,
@@ -731,47 +763,11 @@ export default class Project {
731
763
  const set_statements = sql_column_inserts.map((p) => `${p.column_name} = ?`).join(',');
732
764
  const insert_statement = `UPDATE [class_${class_data.name}] SET ${set_statements} WHERE system_id=${item_id}`;
733
765
  this.db.prepare(insert_statement).run(params);
734
- function validate(input, data_type, max_values) {
735
- const multiple = max_values == null || max_values > 1;
736
- const values = multiple ? input : [input];
737
- if (!Array.isArray(values)) {
738
- return { valid: false, message: 'Expecting array, got single value' };
739
- }
740
- const validated_values = [];
741
- for (let value of values) {
742
- if (real_data_types.includes(data_type) || integer_data_types.includes(data_type)) {
743
- if (data_type == 'boolean') {
744
- if (typeof value == 'boolean' || [0, 1].includes(value)) {
745
- validated_values.push(+value);
746
- }
747
- else {
748
- return { valid: false, message: `Expecting boolean or binary integer, got "${value}" (${typeof value})` };
749
- }
750
- }
751
- else if (typeof value == 'number') {
752
- validated_values.push(value);
753
- }
754
- else {
755
- return { valid: false, message: `Expecting number, got "${value}" (${typeof value})` };
756
- }
757
- }
758
- else if (text_data_types.includes(data_type)) {
759
- // NOTE: could come back to validate resource as links/filepaths later, but leaving unopinionated for now
760
- if (typeof value == 'string') {
761
- validated_values.push(value);
762
- }
763
- else {
764
- return { valid: false, message: `Expecting string, got "${value}" (${typeof value})` };
765
- }
766
- }
767
- }
768
- const output = multiple ? JSON.stringify(validated_values) : validated_values[0];
769
- return {
770
- valid: true,
771
- output
772
- };
773
- }
774
766
  }
767
+ /**
768
+ * Adds/removes relations between items/item properties
769
+ * @param relations - list of pairs of items for which relations should be added or removed between specified properties
770
+ */
775
771
  action_edit_relations(relations) {
776
772
  // NOTE: changes to make to this in the future:
777
773
  // - 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
@@ -808,27 +804,29 @@ export default class Project {
808
804
  }
809
805
  // MARKER: modify item retrieval
810
806
  retrieve_class_items({ class_id, class_name, class_data, pagination = {} }) {
807
+ // 1. DETERMINE COLUMNS AND ROWS TO RETRIEVE ---------------------------------------------------
811
808
  var _a, _b, _c, _d, _e;
812
809
  const pagination_defaults = {
813
810
  page_size: null,
814
811
  property_range: 'all',
815
- item_range: 'all'
812
+ item_range: 'all',
813
+ conditions: []
816
814
  };
815
+ // set pagination rules by overriding defaults with any custom settings
817
816
  pagination = Object.assign(Object.assign({}, pagination_defaults), pagination);
817
+ // get class data+name if not already passed in
818
818
  if (class_name == undefined || class_data == undefined) {
819
819
  class_data = this.lookup_class(class_id);
820
820
  class_name = class_data.name;
821
821
  }
822
822
  ;
823
- const class_string = `[class_${class_name}]`;
824
- // joined+added at beginning of the query, built from relations
825
- const cte_strings = [];
826
- // joined+added near the end of the query, built from relations
827
- const cte_joins = [];
828
- // joined+added between SELECT and FROM, built from relations
829
- const relation_selections = [];
823
+ // gets the label prop for this class
830
824
  const label_prop_ids = (_b = (_a = class_data.metadata.label) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : [];
831
- // if a property_range is defined, first filter class_data.properties by those IDs
825
+ const where_conditions = [];
826
+ if (pagination.item_range && pagination.item_range !== 'all') {
827
+ where_conditions.push(`system_id in (${pagination.item_range.join(',')})`);
828
+ }
829
+ // if a property_range is defined, first filter properties retrieved by those IDs
832
830
  const retrieved_properties = class_data.properties.filter((prop) => {
833
831
  if (pagination.property_range == 'all' || !pagination.property_range) {
834
832
  return true;
@@ -843,22 +841,51 @@ export default class Project {
843
841
  return true;
844
842
  }
845
843
  });
844
+ const cte_properties = [];
845
+ for (let condition of (pagination.conditions || [])) {
846
+ if (condition.name == 'under_property_max') {
847
+ const property = class_data.properties.find((p) => p.id == condition.property_id);
848
+ if ((property === null || property === void 0 ? void 0 : property.type) == 'relation' && property.max_values !== null) {
849
+ if (!cte_properties.some((p) => p.id == condition.property_id) && !retrieved_properties.some((p) => p.id == condition.property_id)) {
850
+ // if this property isn’t retrieved directly, we have to make sure a CTE is created for it, just so we can count the values
851
+ cte_properties.push(Object.assign(Object.assign({}, property), { cte_only: true }));
852
+ }
853
+ // add a condition that counts the items selected by this item for this property
854
+ where_conditions.push(`COALESCE([count_user_${property.name}],0) < ${property.max_values}`);
855
+ }
856
+ }
857
+ }
858
+ // separates these, since they are handled separately in the query
846
859
  const relation_properties = retrieved_properties.filter(a => a.type == 'relation');
847
860
  const data_properties = retrieved_properties.filter(a => a.type == 'data');
848
- for (let prop of relation_properties) {
861
+ // 2. GENERATE SQLITE QUERY --------------------------------------------------
862
+ // class table name in db
863
+ const class_string = `[class_${class_name}]`;
864
+ // 2a. Handle relation properties by generating common table expressions (ctes)
865
+ // joined+added at beginning of the query, built from relations
866
+ const cte_strings = [];
867
+ // joined+added near the end of the query, built from relations
868
+ const cte_joins = [];
869
+ // joined+added between SELECT and FROM, built from relations
870
+ const relation_selections = [];
871
+ for (let prop of [...relation_properties, ...cte_properties]) {
849
872
  const target_selects = [];
850
- let property_junction_column_name = junction_col_name(class_id, prop.id);
873
+ // name of column for this class/property in junction table
874
+ const property_junction_column_name = junction_col_name(class_id, prop.id);
875
+ // loop through each target
851
876
  if (prop.relation_targets.length > 0) {
852
877
  for (let i = 0; i < prop.relation_targets.length; i++) {
853
878
  // find the side that does not match both the class and prop IDs
854
- let target = prop.relation_targets[i];
879
+ const target = prop.relation_targets[i];
855
880
  const target_class = this.class_cache.find((a) => a.id == (target === null || target === void 0 ? void 0 : target.class_id));
856
881
  if (target && target_class) {
882
+ // target column name in junction table
857
883
  let target_junction_column_name = junction_col_name(target.class_id, target.prop_id);
858
- // NOTE: as mentioned elsewhere, possibly allow multiple label props
884
+ // get label of label property in target prop
859
885
  const target_label_id = (_d = (_c = target_class === null || target_class === void 0 ? void 0 : target_class.metadata) === null || _c === void 0 ? void 0 : _c.label) === null || _d === void 0 ? void 0 : _d.properties[0];
860
886
  const target_label = target_class === null || target_class === void 0 ? void 0 : target_class.properties.find((p) => p.id == target_label_id);
861
887
  const label_sql_string = target_label ? `,'user_${target_label.name}',target_class."user_${target_label.name}"` : '';
888
+ // NOTE: as mentioned elsewhere, possibly allow multiple label props
862
889
  let junction_id = target.junction_id;
863
890
  let target_select = `
864
891
  SELECT
@@ -875,7 +902,7 @@ export default class Project {
875
902
  }
876
903
  // uses built-in aggregate json function instead of group_concat craziness
877
904
  const cte = `[${prop.id}_cte] AS (
878
- SELECT "${property_junction_column_name}", json_group_array( json(target_data) ) AS [user_${prop.name}]
905
+ SELECT "${property_junction_column_name}", json_group_array( json(target_data) ) AS [user_${prop.name}], COUNT(1) AS [count_user_${prop.name}]
879
906
  FROM
880
907
  (
881
908
  ${target_selects.join(`
@@ -887,30 +914,28 @@ export default class Project {
887
914
 
888
915
  )`;
889
916
  cte_strings.push(cte);
890
- relation_selections.push(`[${prop.id}_cte].[user_${prop.name}]`);
917
+ if (!("cte_only" in prop && prop.cte_only))
918
+ relation_selections.push(`[${prop.id}_cte].[user_${prop.name}]`);
891
919
  cte_joins.push(`LEFT JOIN [${prop.id}_cte] ON [${prop.id}_cte]."${property_junction_column_name}" = ${class_string}.system_id`);
892
920
  }
893
921
  else {
894
- relation_selections.push(`'[]' AS [user_${prop.name}]`);
922
+ if (!("cte_only" in prop && prop.cte_only))
923
+ relation_selections.push(`'[]' AS [user_${prop.name}]`);
895
924
  }
896
925
  }
897
926
  let orderby = `ORDER BY ${class_string}.system_order`;
898
927
  const data_prop_sql_string = data_properties.length > 0 ? ', ' + data_properties.map((p) => `[user_${p.name}]`).join(',') : '';
899
928
  const table_selection = pagination.property_range == 'all' ? `[class_${class_name}].*` : `system_id,system_order${data_prop_sql_string}`;
900
- let filter_by_items = '';
901
- if (pagination.item_range && pagination.item_range !== 'all') {
902
- filter_by_items = `WHERE system_id in (${pagination.item_range.join(',')})`;
903
- }
904
- let comma_break = `,
929
+ const where_string = where_conditions.length > 0 ? `WHERE ${where_conditions.map((w) => `(${w})`).join(' AND ')}` : '';
930
+ const comma_break = `,
905
931
  `;
906
932
  let query = `
907
933
  ${cte_strings.length > 0 ? "WITH " + cte_strings.join(comma_break) : ''}
908
934
  SELECT ${table_selection} ${relation_selections.length > 0 ? ', ' + relation_selections.join(`, `) : ''}
909
935
  FROM [class_${class_name}]
910
936
  ${cte_joins.join(' ')}
911
- ${filter_by_items}
937
+ ${where_string}
912
938
  ${orderby}`;
913
- console.log('query', query);
914
939
  // possibly elaborate this any type a little more in the future, e.g. a CellValue or SQLCellValue type that expects some wildcards
915
940
  let items = this.db.prepare(query).all();
916
941
  let stringified_properties = class_data.properties.filter(a => a.type == 'relation' || can_have_multiple_values(a.max_values));
package/dist/types.d.ts CHANGED
@@ -142,6 +142,10 @@ export type ClassData = {
142
142
  properties: Property[];
143
143
  };
144
144
  export type ClassList = ClassData[];
145
+ type ConditionUnderPropertyMax = {
146
+ name: 'under_property_max';
147
+ property_id: number;
148
+ };
145
149
  export type ItemPagination = {
146
150
  page_size?: number | null;
147
151
  page_range?: [start: number, end?: number];
@@ -149,6 +153,7 @@ export type ItemPagination = {
149
153
  property_range?: number[] | 'slim' | 'all';
150
154
  /** Filter by item IDs. If not specified, pulls all */
151
155
  item_range?: number[] | 'all';
156
+ conditions?: ConditionUnderPropertyMax[];
152
157
  };
153
158
  export type PaginatedItems = ItemPagination & {
154
159
  loaded: ClassRow[];
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { JunctionSides, RelationshipSide, MaxValues, RelationshipSideBase, RelationEdit, RelationEditValidSides, ClassData, JunctionList } from "./types.js";
1
+ import { JunctionSides, RelationshipSide, MaxValues, RelationshipSideBase, RelationEdit, RelationEditValidSides, ClassData, JunctionList, DataType } from "./types.js";
2
2
  export declare function defined<T>(v: T): v is NonNullable<T>;
3
+ export declare const text_data_types: string[];
4
+ export declare const integer_data_types: string[];
5
+ export declare const real_data_types: string[];
3
6
  export declare function partial_relation_match(old_relation: JunctionSides, new_relation: JunctionSides): boolean;
4
7
  export declare function side_match(x: RelationshipSide, y: RelationshipSide): boolean;
5
8
  export declare function full_relation_match(a: JunctionSides, b: JunctionSides): boolean;
@@ -11,3 +14,10 @@ export declare function junction_col_name(class_id: number, prop_id: number | un
11
14
  export declare function readable_side(side: RelationshipSide, classlist: ClassData[]): string;
12
15
  export declare function readable_edit(edit: RelationEditValidSides, classlist: ClassData[]): string | undefined;
13
16
  export declare function readable_junctionlist(relationships: JunctionList, classlist: ClassData[]): string[];
17
+ export declare function validate_data_value(input: any, data_type: DataType, max_values: MaxValues): {
18
+ valid: true;
19
+ output: string | number;
20
+ } | {
21
+ valid: false;
22
+ message: string;
23
+ };
package/dist/utils.js CHANGED
@@ -1,6 +1,9 @@
1
1
  export function defined(v) {
2
2
  return v !== undefined && v !== null;
3
3
  }
4
+ export const text_data_types = ['string', 'resource'];
5
+ export const integer_data_types = ['boolean'];
6
+ export const real_data_types = ['number'];
4
7
  // given the type above,
5
8
  // check if two relations share class ids on both sides
6
9
  // and share a property id on at least one side
@@ -103,4 +106,44 @@ export function readable_junctionlist(relationships, classlist) {
103
106
  return readable_sides(r.sides, classlist);
104
107
  });
105
108
  }
109
+ export function validate_data_value(input, data_type, max_values) {
110
+ const multiple = max_values == null || max_values > 1;
111
+ const values = multiple ? input : [input];
112
+ if (!Array.isArray(values)) {
113
+ return { valid: false, message: 'Expecting array, got single value' };
114
+ }
115
+ const validated_values = [];
116
+ for (let value of values) {
117
+ if (real_data_types.includes(data_type) || integer_data_types.includes(data_type)) {
118
+ if (data_type == 'boolean') {
119
+ if (typeof value == 'boolean' || [0, 1].includes(value)) {
120
+ validated_values.push(+value);
121
+ }
122
+ else {
123
+ return { valid: false, message: `Expecting boolean or binary integer, got "${value}" (${typeof value})` };
124
+ }
125
+ }
126
+ else if (typeof value == 'number') {
127
+ validated_values.push(value);
128
+ }
129
+ else {
130
+ return { valid: false, message: `Expecting number, got "${value}" (${typeof value})` };
131
+ }
132
+ }
133
+ else if (text_data_types.includes(data_type)) {
134
+ // NOTE: could come back to validate resource as links/filepaths later, but leaving unopinionated for now
135
+ if (typeof value == 'string') {
136
+ validated_values.push(value);
137
+ }
138
+ else {
139
+ return { valid: false, message: `Expecting string, got "${value}" (${typeof value})` };
140
+ }
141
+ }
142
+ }
143
+ const output = multiple ? JSON.stringify(validated_values) : validated_values[0];
144
+ return {
145
+ valid: true,
146
+ output
147
+ };
148
+ }
106
149
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goby-database",
3
- "version": "2.2.28",
3
+ "version": "2.2.30",
4
4
  "description": "This will hold the core better-sqlite3-powered application for creating and modifying goby databases",
5
5
  "main": "dist/index.js",
6
6
  "files": [