goby-database 2.2.29 → 2.2.31

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
@@ -97,7 +97,11 @@ export default class Project {
97
97
  */
98
98
  action_edit_relations(relations: {
99
99
  change: 'add' | 'remove';
100
- sides: [input_1: ItemRelationSide, input_2: ItemRelationSide];
100
+ sides: [input_1: ItemRelationSide & {
101
+ order?: number;
102
+ }, input_2: ItemRelationSide & {
103
+ order?: number;
104
+ }];
101
105
  }[]): void;
102
106
  retrieve_class_items({ class_id, class_name, class_data, pagination }: {
103
107
  class_id: number;
package/dist/index.js CHANGED
@@ -14,7 +14,8 @@ export default class Project {
14
14
  else {
15
15
  console.log('opened goby database');
16
16
  }
17
- //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
18
19
  this.run = {
19
20
  begin: this.db.prepare('BEGIN IMMEDIATE'),
20
21
  commit: this.db.prepare('COMMIT'),
@@ -598,12 +599,20 @@ export default class Project {
598
599
  let id = (_a = this.db.prepare('SELECT id FROM system_junctionlist ORDER BY id DESC').get()) === null || _a === void 0 ? void 0 : _a.id;
599
600
  if (typeof id !== 'number')
600
601
  throw new Error('Something went wrong creating a new relationship');
602
+ const side_0_col = junction_col_name(sides[0].class_id, sides[0].prop_id);
603
+ const side_1_col = junction_col_name(sides[1].class_id, sides[1].prop_id);
604
+ const columns = [
605
+ `"${side_0_col}" INTEGER`,
606
+ `"${side_1_col}" INTEGER`,
607
+ ];
608
+ // for each side, check if it has a prop
609
+ // if it does, add column for its order
610
+ if (defined(sides[0].prop_id))
611
+ columns.push(`"${side_0_col}_order" REAL`);
612
+ if (defined(sides[1].prop_id))
613
+ columns.push(`"${side_1_col}_order" REAL`);
601
614
  // creates table
602
- this.create_table('junction', id, [
603
- `"${junction_col_name(sides[0].class_id, sides[0].prop_id)}" INTEGER`,
604
- `"${junction_col_name(sides[1].class_id, sides[1].prop_id)}" INTEGER`,
605
- `date_added INTEGER`
606
- ]);
615
+ this.create_table('junction', id, columns);
607
616
  return id;
608
617
  }
609
618
  transfer_connections(source, target) {
@@ -779,51 +788,98 @@ export default class Project {
779
788
  input_2: junction_col_name(input_2.class_id, input_2.prop_id)
780
789
  };
781
790
  const 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;
782
- if (junction_id) {
783
- if (change == 'add') {
784
- const date_added = Date.now();
785
- this.db.prepare(`
786
- INSERT INTO junction_${junction_id}
787
- ("${column_names.input_1}", "${column_names.input_2}",date_added)
788
- VALUES (${input_1.item_id},${input_2.item_id},${date_added})
789
- `).run();
790
- }
791
- else if (change == 'remove') {
792
- this.db.prepare(`
793
- DELETE FROM junction_${junction_id}
794
- WHERE "${column_names.input_1}" = ${input_1.item_id}
795
- AND "${column_names.input_2}" = ${input_2.item_id}`).run();
796
- }
797
- }
798
- else {
791
+ if (!defined(junction_id)) {
799
792
  throw Error('Something went wrong - junction table for relationship not found');
800
793
  }
794
+ if (change == 'add') {
795
+ const column_value_set = [
796
+ {
797
+ column: `"${column_names.input_1}"`,
798
+ value: input_1.item_id
799
+ },
800
+ {
801
+ column: `"${column_names.input_2}"`,
802
+ value: input_2.item_id
803
+ }
804
+ ];
805
+ sides.forEach((side, s) => {
806
+ var _a, _b, _c;
807
+ // for each side, check if it has a prop id
808
+ if (defined(side.prop_id)) {
809
+ // if it does, prepare to add a column entry for it
810
+ const column = `"${s == 0 ? column_names.input_1 : column_names.input_2}_order"`;
811
+ let value = 0;
812
+ // check if it has an order set
813
+ if (defined(side.order)) {
814
+ // if so, use that for the column entry
815
+ value = side.order;
816
+ }
817
+ else {
818
+ // if not, you have to do a lookup of that property value for that item, and get the last order value
819
+ const class_data = this.lookup_class(side.class_id);
820
+ const item = (_a = this.retrieve_class_items({
821
+ class_id: side.class_id,
822
+ class_data,
823
+ pagination: {
824
+ item_range: [side.item_id],
825
+ property_range: [side.prop_id]
826
+ }
827
+ }).loaded) === null || _a === void 0 ? void 0 : _a[0];
828
+ const prop_name = (_b = class_data.properties.find((p) => p.id == side.prop_id)) === null || _b === void 0 ? void 0 : _b.name;
829
+ const prop_selections = defined(prop_name) && item ? item[`user_${prop_name}`] : [];
830
+ if (Array.isArray(prop_selections)) {
831
+ const last_item_order = (_c = prop_selections === null || prop_selections === void 0 ? void 0 : prop_selections.at(-1)) === null || _c === void 0 ? void 0 : _c.system_order;
832
+ if (typeof last_item_order == 'number') {
833
+ value = last_item_order + 1000;
834
+ }
835
+ }
836
+ }
837
+ column_value_set.push({
838
+ column,
839
+ value
840
+ });
841
+ }
842
+ });
843
+ this.db.prepare(`
844
+ INSERT INTO junction_${junction_id}
845
+ (${column_value_set.map((a) => a.column).join(',')})
846
+ VALUES (${column_value_set.map((a) => a.value).join(',')})
847
+ `).run();
848
+ }
849
+ else if (change == 'remove') {
850
+ this.db.prepare(`
851
+ DELETE FROM junction_${junction_id}
852
+ WHERE "${column_names.input_1}" = ${input_1.item_id}
853
+ AND "${column_names.input_2}" = ${input_2.item_id}`).run();
854
+ }
801
855
  }
802
856
  // NOTE: should this trigger a refresh to items?
803
857
  }
804
858
  // MARKER: modify item retrieval
805
859
  retrieve_class_items({ class_id, class_name, class_data, pagination = {} }) {
860
+ // 1. DETERMINE COLUMNS AND ROWS TO RETRIEVE ---------------------------------------------------
806
861
  var _a, _b, _c, _d, _e;
807
862
  const pagination_defaults = {
808
863
  page_size: null,
809
864
  property_range: 'all',
810
- item_range: 'all'
865
+ item_range: 'all',
866
+ conditions: []
811
867
  };
868
+ // set pagination rules by overriding defaults with any custom settings
812
869
  pagination = Object.assign(Object.assign({}, pagination_defaults), pagination);
870
+ // get class data+name if not already passed in
813
871
  if (class_name == undefined || class_data == undefined) {
814
872
  class_data = this.lookup_class(class_id);
815
873
  class_name = class_data.name;
816
874
  }
817
875
  ;
818
- const class_string = `[class_${class_name}]`;
819
- // joined+added at beginning of the query, built from relations
820
- const cte_strings = [];
821
- // joined+added near the end of the query, built from relations
822
- const cte_joins = [];
823
- // joined+added between SELECT and FROM, built from relations
824
- const relation_selections = [];
876
+ // gets the label prop for this class
825
877
  const label_prop_ids = (_b = (_a = class_data.metadata.label) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : [];
826
- // if a property_range is defined, first filter class_data.properties by those IDs
878
+ const where_conditions = [];
879
+ if (pagination.item_range && pagination.item_range !== 'all') {
880
+ where_conditions.push(`system_id in (${pagination.item_range.join(',')})`);
881
+ }
882
+ // if a property_range is defined, first filter properties retrieved by those IDs
827
883
  const retrieved_properties = class_data.properties.filter((prop) => {
828
884
  if (pagination.property_range == 'all' || !pagination.property_range) {
829
885
  return true;
@@ -838,27 +894,61 @@ export default class Project {
838
894
  return true;
839
895
  }
840
896
  });
897
+ const cte_properties = [];
898
+ for (let condition of (pagination.conditions || [])) {
899
+ if (condition.name == 'under_property_max') {
900
+ const property = class_data.properties.find((p) => p.id == condition.property_id);
901
+ if ((property === null || property === void 0 ? void 0 : property.type) == 'relation' && property.max_values !== null) {
902
+ if (!cte_properties.some((p) => p.id == condition.property_id) && !retrieved_properties.some((p) => p.id == condition.property_id)) {
903
+ // 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
904
+ cte_properties.push(Object.assign(Object.assign({}, property), { cte_only: true }));
905
+ }
906
+ // add a condition that counts the items selected by this item for this property
907
+ where_conditions.push(`COALESCE([count_user_${property.name}],0) < ${property.max_values}`);
908
+ }
909
+ }
910
+ }
911
+ // separates these, since they are handled separately in the query
841
912
  const relation_properties = retrieved_properties.filter(a => a.type == 'relation');
842
913
  const data_properties = retrieved_properties.filter(a => a.type == 'data');
843
- for (let prop of relation_properties) {
914
+ // 2. GENERATE SQLITE QUERY --------------------------------------------------
915
+ // class table name in db
916
+ const class_string = `[class_${class_name}]`;
917
+ // 2a. Handle relation properties by generating common table expressions (ctes)
918
+ // joined+added at beginning of the query, built from relations
919
+ const cte_strings = [];
920
+ // joined+added near the end of the query, built from relations
921
+ const cte_joins = [];
922
+ // joined+added between SELECT and FROM, built from relations
923
+ const relation_selections = [];
924
+ for (let prop of [...relation_properties, ...cte_properties]) {
844
925
  const target_selects = [];
845
- let property_junction_column_name = junction_col_name(class_id, prop.id);
926
+ // name of column for this class/property in junction table
927
+ const property_junction_column_name = junction_col_name(class_id, prop.id);
928
+ // loop through each target
846
929
  if (prop.relation_targets.length > 0) {
847
930
  for (let i = 0; i < prop.relation_targets.length; i++) {
848
931
  // find the side that does not match both the class and prop IDs
849
- let target = prop.relation_targets[i];
932
+ const target = prop.relation_targets[i];
850
933
  const target_class = this.class_cache.find((a) => a.id == (target === null || target === void 0 ? void 0 : target.class_id));
851
934
  if (target && target_class) {
935
+ // target column name in junction table
852
936
  let target_junction_column_name = junction_col_name(target.class_id, target.prop_id);
853
- // NOTE: as mentioned elsewhere, possibly allow multiple label props
937
+ // get label of label property in target prop
854
938
  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];
855
939
  const target_label = target_class === null || target_class === void 0 ? void 0 : target_class.properties.find((p) => p.id == target_label_id);
856
940
  const label_sql_string = target_label ? `,'user_${target_label.name}',target_class."user_${target_label.name}"` : '';
941
+ // NOTE: as mentioned elsewhere, possibly allow multiple label props
857
942
  let junction_id = target.junction_id;
858
943
  let target_select = `
859
944
  SELECT
860
945
  "${property_junction_column_name}",
861
- json_object('class_id',${target.class_id},'system_id',junction."${target_junction_column_name}"${label_sql_string}) AS target_data, junction.date_added AS date_added
946
+ json_object(
947
+ 'class_id',${target.class_id},
948
+ 'system_id',junction."${target_junction_column_name}",
949
+ 'system_order',junction."${property_junction_column_name}_order"
950
+ ${label_sql_string}
951
+ ) AS target_data, junction."${property_junction_column_name}_order" AS relation_order
862
952
  FROM junction_${junction_id} AS junction
863
953
  LEFT JOIN "class_${target_class === null || target_class === void 0 ? void 0 : target_class.name}" AS target_class ON junction."${target_junction_column_name}" = target_class.system_id
864
954
  `;
@@ -870,40 +960,39 @@ export default class Project {
870
960
  }
871
961
  // uses built-in aggregate json function instead of group_concat craziness
872
962
  const cte = `[${prop.id}_cte] AS (
873
- SELECT "${property_junction_column_name}", json_group_array( json(target_data) ) AS [user_${prop.name}]
963
+ SELECT "${property_junction_column_name}", json_group_array( json(target_data) ) AS [user_${prop.name}], COUNT(1) AS [count_user_${prop.name}]
874
964
  FROM
875
965
  (
876
966
  ${target_selects.join(`
877
967
  UNION
878
968
  `)}
879
- ORDER BY date_added
969
+ ORDER BY relation_order
880
970
  )
881
971
  GROUP BY "${property_junction_column_name}"
882
972
 
883
973
  )`;
884
974
  cte_strings.push(cte);
885
- relation_selections.push(`[${prop.id}_cte].[user_${prop.name}]`);
975
+ if (!("cte_only" in prop && prop.cte_only))
976
+ relation_selections.push(`[${prop.id}_cte].[user_${prop.name}]`);
886
977
  cte_joins.push(`LEFT JOIN [${prop.id}_cte] ON [${prop.id}_cte]."${property_junction_column_name}" = ${class_string}.system_id`);
887
978
  }
888
979
  else {
889
- relation_selections.push(`'[]' AS [user_${prop.name}]`);
980
+ if (!("cte_only" in prop && prop.cte_only))
981
+ relation_selections.push(`'[]' AS [user_${prop.name}]`);
890
982
  }
891
983
  }
892
984
  let orderby = `ORDER BY ${class_string}.system_order`;
893
985
  const data_prop_sql_string = data_properties.length > 0 ? ', ' + data_properties.map((p) => `[user_${p.name}]`).join(',') : '';
894
986
  const table_selection = pagination.property_range == 'all' ? `[class_${class_name}].*` : `system_id,system_order${data_prop_sql_string}`;
895
- let filter_by_items = '';
896
- if (pagination.item_range && pagination.item_range !== 'all') {
897
- filter_by_items = `WHERE system_id in (${pagination.item_range.join(',')})`;
898
- }
899
- let comma_break = `,
987
+ const where_string = where_conditions.length > 0 ? `WHERE ${where_conditions.map((w) => `(${w})`).join(' AND ')}` : '';
988
+ const comma_break = `,
900
989
  `;
901
990
  let query = `
902
991
  ${cte_strings.length > 0 ? "WITH " + cte_strings.join(comma_break) : ''}
903
992
  SELECT ${table_selection} ${relation_selections.length > 0 ? ', ' + relation_selections.join(`, `) : ''}
904
993
  FROM [class_${class_name}]
905
994
  ${cte_joins.join(' ')}
906
- ${filter_by_items}
995
+ ${where_string}
907
996
  ${orderby}`;
908
997
  // possibly elaborate this any type a little more in the future, e.g. a CellValue or SQLCellValue type that expects some wildcards
909
998
  let items = this.db.prepare(query).all();
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goby-database",
3
- "version": "2.2.29",
3
+ "version": "2.2.31",
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": [