json-fs-query 0.1.1 → 1.0.0

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check whether any other record, in any entity, still has a single-field foreign key pointing
3
+ * at entity.property = value - the mirror image of foreignKeyExists(), which checks the opposite
4
+ * direction (that a new/updated foreign key value points at something real). Used to RESTRICT a
5
+ * delete or update that would otherwise leave a dangling reference behind. Scans every record in
6
+ * the database via the __RECORDS registry, the same "no index needed, just look" approach as
7
+ * keyUnique()/foreignKeyExists() - correctness over speed, matching this project's overall scale.
8
+ * @param entity
9
+ * @param property
10
+ * @param value
11
+ */
12
+ export declare const hasDependents: (entity: string, property: string, value: any) => Promise<boolean>;
13
+ //# sourceMappingURL=hasDependents.d.ts.map
@@ -0,0 +1,52 @@
1
+ 'use strict'
2
+
3
+ Object.defineProperty(exports, '__esModule', {
4
+ value: true
5
+ })
6
+ exports.hasDependents = void 0
7
+ const _describe = require('../describe')
8
+ const _retrieveRecord = require('../../utilities/retrieveRecord')
9
+ const _retrieveRecords = require('../../utilities/retrieveRecords')
10
+ const _where = require('../../queries/where')
11
+ const _splitEntityProperty = require('../../utilities/parsers/splitEntityProperty')
12
+ /**
13
+ * Check whether any other record, in any entity, still has a single-field foreign key pointing
14
+ * at entity.property = value - the mirror image of foreignKeyExists(), which checks the opposite
15
+ * direction (that a new/updated foreign key value points at something real). Used to RESTRICT a
16
+ * delete or update that would otherwise leave a dangling reference behind. Scans every record in
17
+ * the database via the __RECORDS registry, the same "no index needed, just look" approach as
18
+ * keyUnique()/foreignKeyExists() - correctness over speed, matching this project's overall scale.
19
+ * @param entity
20
+ * @param property
21
+ * @param value
22
+ */
23
+ const hasDependents = async (entity, property, value) => {
24
+ const entityFiles = await (0, _describe.describe)()
25
+ for (const entityFile of entityFiles) {
26
+ const otherEntity = entityFile.replace(/\.json$/, '')
27
+ const otherRecord = await (0, _retrieveRecord.retrieveRecord)(otherEntity)
28
+ for (const key of otherRecord.keys) {
29
+ if (key.type !== 'foreign' || key.fields.length !== 1 || !key.references || key.references.length !== 1) {
30
+ continue
31
+ }
32
+ const {
33
+ entity: refEntity,
34
+ property: refProperty
35
+ } = (0, _splitEntityProperty.splitEntityProperty)(key.references[0].fields[0])
36
+ if (refEntity !== entity || refProperty !== property) {
37
+ continue
38
+ }
39
+ const records = await (0, _retrieveRecords.retrieveRecords)(otherEntity)
40
+ const found = (0, _where.where)(records, {
41
+ property: key.fields[0],
42
+ comparator: '=',
43
+ value
44
+ })
45
+ if (found.length) {
46
+ return true
47
+ }
48
+ }
49
+ }
50
+ return false
51
+ }
52
+ exports.hasDependents = hasDependents
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.hasDependents=void 0;var _describe=require("../describe"),_retrieveRecord=require("../../utilities/retrieveRecord"),_retrieveRecords=require("../../utilities/retrieveRecords"),_where=require("../../queries/where"),_splitEntityProperty=require("../../utilities/parsers/splitEntityProperty");const hasDependents=async(e,r,t)=>{const i=await(0,_describe.describe)();for(const s of i){const i=s.replace(/\.json$/,""),o=await(0,_retrieveRecord.retrieveRecord)(i);for(const s of o.keys){if("foreign"!==s.type||1!==s.fields.length||!s.references||1!==s.references.length)continue;const{entity:o,property:n}=(0,_splitEntityProperty.splitEntityProperty)(s.references[0].fields[0]);if(o!==e||n!==r)continue;const c=await(0,_retrieveRecords.retrieveRecords)(i);if((0,_where.where)(c,{property:s.fields[0],comparator:"=",value:t}).length)return!0}}return!1};exports.hasDependents=hasDependents;
@@ -10,7 +10,7 @@ const keyGenerate = async (entity, references = []) => {
10
10
  let isUnique = false
11
11
  while (!isUnique) {
12
12
  uuid = crypto.randomUUID()
13
- isUnique = await (0, _keyUnique.keyUnique)(entity, references, uuid)
13
+ isUnique = await (0, _keyUnique.keyUnique)(entity, references, [uuid])
14
14
  }
15
15
  return uuid
16
16
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.keyGenerate=void 0;var _keyUnique=require("./keyUnique");const keyGenerate=async(e,r=[])=>{let t=null,n=!1;for(;!n;)t=crypto.randomUUID(),n=await(0,_keyUnique.keyUnique)(e,r,t);return t};exports.keyGenerate=keyGenerate;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.keyGenerate=void 0;var _keyUnique=require("./keyUnique");const keyGenerate=async(e,r=[])=>{let t=null,n=!1;for(;!n;)t=crypto.randomUUID(),n=await(0,_keyUnique.keyUnique)(e,r,[t]);return t};exports.keyGenerate=keyGenerate;
@@ -1,2 +1,10 @@
1
- export declare const keyUnique: (entity: any, references?: any[], checkKey?: any) => Promise<boolean>;
1
+ /**
2
+ * Check that no existing record already has this combination of values across all of the given
3
+ * fields - a single field for a normal unique/primary key, or several for a composite one.
4
+ * references and checkKeys are parallel arrays: references[i]'s value must equal checkKeys[i].
5
+ * @param entity
6
+ * @param references
7
+ * @param checkKeys
8
+ */
9
+ export declare const keyUnique: (entity: any, references?: any[], checkKeys?: any[]) => Promise<boolean>;
2
10
  //# sourceMappingURL=keyUnique.d.ts.map
@@ -6,16 +6,26 @@ Object.defineProperty(exports, '__esModule', {
6
6
  exports.keyUnique = void 0
7
7
  const _retrieveRecords = require('../../utilities/retrieveRecords')
8
8
  const _where = require('../../queries/where')
9
- const keyUnique = async (entity, references = [], checkKey = null) => {
10
- if (references.length !== 1) {
9
+ /**
10
+ * Check that no existing record already has this combination of values across all of the given
11
+ * fields - a single field for a normal unique/primary key, or several for a composite one.
12
+ * references and checkKeys are parallel arrays: references[i]'s value must equal checkKeys[i].
13
+ * @param entity
14
+ * @param references
15
+ * @param checkKeys
16
+ */
17
+ const keyUnique = async (entity, references = [], checkKeys = []) => {
18
+ if (!references.length || references.length !== checkKeys.length) {
11
19
  return false
12
20
  }
13
- const records = await (0, _retrieveRecords.retrieveRecords)(entity)
14
- const found = (0, _where.where)(records, {
15
- property: references[0],
16
- comparator: '=',
17
- value: checkKey
18
- })
19
- return !found.length
21
+ let records = await (0, _retrieveRecords.retrieveRecords)(entity)
22
+ for (let i = 0; i < references.length; i++) {
23
+ records = (0, _where.where)(records, {
24
+ property: references[i],
25
+ comparator: '=',
26
+ value: checkKeys[i]
27
+ })
28
+ }
29
+ return !records.length
20
30
  }
21
31
  exports.keyUnique = keyUnique
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.keyUnique=void 0;var _retrieveRecords=require("../../utilities/retrieveRecords"),_where=require("../../queries/where");const keyUnique=async(e,r=[],t=null)=>{if(1!==r.length)return!1;const i=await(0,_retrieveRecords.retrieveRecords)(e);return!(0,_where.where)(i,{property:r[0],comparator:"=",value:t}).length};exports.keyUnique=keyUnique;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.keyUnique=void 0;var _retrieveRecords=require("../../utilities/retrieveRecords"),_where=require("../../queries/where");const keyUnique=async(e,r=[],t=[])=>{if(!r.length||r.length!==t.length)return!1;let i=await(0,_retrieveRecords.retrieveRecords)(e);for(let e=0;e<r.length;e++)i=(0,_where.where)(i,{property:r[e],comparator:"=",value:t[e]});return!i.length};exports.keyUnique=keyUnique;
@@ -27,34 +27,55 @@ require('core-js/modules/esnext.set.union.js')
27
27
  const _retrieveRecord = require('../utilities/retrieveRecord')
28
28
  const _retrieveFile = require('../utilities/retrieveFile')
29
29
  const _removeFromIndex = require('../utilities/removeFromIndex')
30
+ const _computeKeyValue = require('../utilities/computeKeyValue')
31
+ const _hasDependents = require('../introspect/policies/hasDependents')
30
32
  const _config = require('../utilities/config')
31
33
  const {
32
34
  rm,
33
35
  writeFile
34
36
  } = require('fs/promises')
37
+ /**
38
+ * RESTRICT: reject the whole delete (before removing anything) if any row being deleted is still
39
+ * referenced by a single-field foreign key elsewhere. Only meaningful for a single-field primary
40
+ * key, since foreign key support itself is single-field only - a composite primary key can't be
41
+ * the target of one yet, so there's nothing to check.
42
+ * @param entity
43
+ * @param primaryKey
44
+ * @param toDelete
45
+ */
46
+ const assertNoDependents = async (entity, primaryKey, toDelete) => {
47
+ if (!(0, _config.getSetting)('enforceForeignKeys', true) || primaryKey.fields.length !== 1) {
48
+ return
49
+ }
50
+ const property = primaryKey.fields[0]
51
+ for (const row of toDelete) {
52
+ if (await (0, _hasDependents.hasDependents)(entity, property, row[property])) {
53
+ throw new Error(`Cannot delete from "${entity}" where "${property}" = ${row[property]} - other records still reference it`)
54
+ }
55
+ }
56
+ }
35
57
  /**
36
58
  * A record object carries no memory of which entry file it was read from, and files are not
37
59
  * guaranteed to be named after any field's value, so the only reliable way to find a record's
38
- * file is to re-scan every entry and match it by its primary key value.
60
+ * file is to re-scan every entry and match it by its primary key value(s) - a single field, or a
61
+ * composite of several for a multi-field primary key.
39
62
  * @param entity
40
63
  * @param record
41
- * @param primaryField
64
+ * @param primaryKey
42
65
  * @param deletedValues
43
66
  */
44
- const removeMatchingEntries = async (entity, record, primaryField, deletedValues) => {
67
+ const removeMatchingEntries = async (entity, record, primaryKey, deletedValues) => {
45
68
  const databasePath = (0, _config.getSetting)('databasePath', 'database/')
46
69
  const remainingEntries = []
47
70
  for (const fileName of record.entries) {
48
71
  const data = await (0, _retrieveFile.retrieveFile)(`${record.path}/${fileName}`)
49
- if (!deletedValues.has(data[primaryField])) {
72
+ if (!deletedValues.has((0, _computeKeyValue.computeKeyValue)(primaryKey.fields, data))) {
50
73
  remainingEntries.push(fileName)
51
74
  continue
52
75
  }
53
76
  await rm(`${databasePath}${record.path}/${fileName}`)
54
77
  for (const key of record.keys) {
55
- if (key.fields.length === 1) {
56
- await (0, _removeFromIndex.removeFromIndex)(record.path, key, data[key.fields[0]], fileName)
57
- }
78
+ await (0, _removeFromIndex.removeFromIndex)(record.path, key, (0, _computeKeyValue.computeKeyValue)(key.fields, data), fileName)
58
79
  }
59
80
  }
60
81
  return remainingEntries
@@ -62,17 +83,17 @@ const removeMatchingEntries = async (entity, record, primaryField, deletedValues
62
83
  const deleteEntity = async (entity = '', dataSet = {}) => {
63
84
  const databasePath = (0, _config.getSetting)('databasePath', 'database/')
64
85
  const record = await (0, _retrieveRecord.retrieveRecord)(entity)
65
- const primaryKey = record.keys.find(key => key.type === 'primary' && key.fields.length === 1)
86
+ const primaryKey = record.keys.find(key => key.type === 'primary')
66
87
  if (!primaryKey) {
67
- throw new Error(`Cannot delete from "${entity}" without a single-field primary key`)
88
+ throw new Error(`Cannot delete from "${entity}" without a primary key`)
68
89
  }
69
- const primaryField = primaryKey.fields[0]
70
90
  const toDelete = dataSet[entity] || []
71
91
  if (!toDelete.length) {
72
92
  return dataSet
73
93
  }
74
- const deletedValues = new Set(toDelete.map(row => row[primaryField]))
75
- record.entries = await removeMatchingEntries(entity, record, primaryField, deletedValues)
94
+ await assertNoDependents(entity, primaryKey, toDelete)
95
+ const deletedValues = new Set(toDelete.map(row => (0, _computeKeyValue.computeKeyValue)(primaryKey.fields, row)))
96
+ record.entries = await removeMatchingEntries(entity, record, primaryKey, deletedValues)
76
97
  await writeFile(`${databasePath}${record.path}.json`, JSON.stringify(record, null, 2))
77
98
  return dataSet
78
99
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.deleteEntity=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.set.add-all.js"),require("core-js/modules/esnext.set.delete-all.js"),require("core-js/modules/esnext.set.difference.js"),require("core-js/modules/esnext.set.every.js"),require("core-js/modules/esnext.set.filter.js"),require("core-js/modules/esnext.set.find.js"),require("core-js/modules/esnext.set.intersection.js"),require("core-js/modules/esnext.set.is-disjoint-from.js"),require("core-js/modules/esnext.set.is-subset-of.js"),require("core-js/modules/esnext.set.is-superset-of.js"),require("core-js/modules/esnext.set.join.js"),require("core-js/modules/esnext.set.map.js"),require("core-js/modules/esnext.set.reduce.js"),require("core-js/modules/esnext.set.some.js"),require("core-js/modules/esnext.set.symmetric-difference.js"),require("core-js/modules/esnext.set.union.js");var _retrieveRecord=require("../utilities/retrieveRecord"),_retrieveFile=require("../utilities/retrieveFile"),_removeFromIndex=require("../utilities/removeFromIndex"),_config=require("../utilities/config");const{rm:rm,writeFile:writeFile}=require("fs/promises"),removeMatchingEntries=async(e,r,s,t)=>{const i=(0,_config.getSetting)("databasePath","database/"),o=[];for(const e of r.entries){const n=await(0,_retrieveFile.retrieveFile)(`${r.path}/${e}`);if(t.has(n[s])){await rm(`${i}${r.path}/${e}`);for(const s of r.keys)1===s.fields.length&&await(0,_removeFromIndex.removeFromIndex)(r.path,s,n[s.fields[0]],e)}else o.push(e)}return o},deleteEntity=async(e="",r={})=>{const s=(0,_config.getSetting)("databasePath","database/"),t=await(0,_retrieveRecord.retrieveRecord)(e),i=t.keys.find((e=>"primary"===e.type&&1===e.fields.length));if(!i)throw new Error(`Cannot delete from "${e}" without a single-field primary key`);const o=i.fields[0],n=r[e]||[];if(!n.length)return r;const u=new Set(n.map((e=>e[o])));return t.entries=await removeMatchingEntries(0,t,o,u),await writeFile(`${s}${t.path}.json`,JSON.stringify(t,null,2)),r};exports.deleteEntity=deleteEntity;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.deleteEntity=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.set.add-all.js"),require("core-js/modules/esnext.set.delete-all.js"),require("core-js/modules/esnext.set.difference.js"),require("core-js/modules/esnext.set.every.js"),require("core-js/modules/esnext.set.filter.js"),require("core-js/modules/esnext.set.find.js"),require("core-js/modules/esnext.set.intersection.js"),require("core-js/modules/esnext.set.is-disjoint-from.js"),require("core-js/modules/esnext.set.is-subset-of.js"),require("core-js/modules/esnext.set.is-superset-of.js"),require("core-js/modules/esnext.set.join.js"),require("core-js/modules/esnext.set.map.js"),require("core-js/modules/esnext.set.reduce.js"),require("core-js/modules/esnext.set.some.js"),require("core-js/modules/esnext.set.symmetric-difference.js"),require("core-js/modules/esnext.set.union.js");var _retrieveRecord=require("../utilities/retrieveRecord"),_retrieveFile=require("../utilities/retrieveFile"),_removeFromIndex=require("../utilities/removeFromIndex"),_computeKeyValue=require("../utilities/computeKeyValue"),_hasDependents=require("../introspect/policies/hasDependents"),_config=require("../utilities/config");const{rm:rm,writeFile:writeFile}=require("fs/promises"),assertNoDependents=async(e,r,s)=>{if(!(0,_config.getSetting)("enforceForeignKeys",!0)||1!==r.fields.length)return;const t=r.fields[0];for(const r of s)if(await(0,_hasDependents.hasDependents)(e,t,r[t]))throw new Error(`Cannot delete from "${e}" where "${t}" = ${r[t]} - other records still reference it`)},removeMatchingEntries=async(e,r,s,t)=>{const i=(0,_config.getSetting)("databasePath","database/"),o=[];for(const e of r.entries){const n=await(0,_retrieveFile.retrieveFile)(`${r.path}/${e}`);if(t.has((0,_computeKeyValue.computeKeyValue)(s.fields,n))){await rm(`${i}${r.path}/${e}`);for(const s of r.keys)await(0,_removeFromIndex.removeFromIndex)(r.path,s,(0,_computeKeyValue.computeKeyValue)(s.fields,n),e)}else o.push(e)}return o},deleteEntity=async(e="",r={})=>{const s=(0,_config.getSetting)("databasePath","database/"),t=await(0,_retrieveRecord.retrieveRecord)(e),i=t.keys.find((e=>"primary"===e.type));if(!i)throw new Error(`Cannot delete from "${e}" without a primary key`);const o=r[e]||[];if(!o.length)return r;await assertNoDependents(e,i,o);const n=new Set(o.map((e=>(0,_computeKeyValue.computeKeyValue)(i.fields,e))));return t.entries=await removeMatchingEntries(0,t,i,n),await writeFile(`${s}${t.path}.json`,JSON.stringify(t,null,2)),r};exports.deleteEntity=deleteEntity;
@@ -7,6 +7,7 @@ exports.insertEntity = void 0
7
7
  require('core-js/modules/es.json.stringify.js')
8
8
  require('core-js/modules/esnext.iterator.constructor.js')
9
9
  require('core-js/modules/esnext.iterator.find.js')
10
+ require('core-js/modules/esnext.iterator.map.js')
10
11
  const _retrieveRecord = require('../utilities/retrieveRecord')
11
12
  const _updateIndex = require('../utilities/updateIndex')
12
13
  const _keyGenerate = require('../introspect/policies/keyGenerate')
@@ -15,6 +16,7 @@ const _useDefault = require('../introspect/policies/useDefault')
15
16
  const _typeCheck = require('../introspect/policies/typeCheck')
16
17
  const _foreignKeyExists = require('../introspect/policies/foreignKeyExists')
17
18
  const _splitEntityProperty = require('../utilities/parsers/splitEntityProperty')
19
+ const _computeKeyValue = require('../utilities/computeKeyValue')
18
20
  const _config = require('../utilities/config')
19
21
  const {
20
22
  writeFile
@@ -46,19 +48,21 @@ const buildEntityValues = async (entity, record, rowValues = {}) => {
46
48
  return entityValues
47
49
  }
48
50
  /**
49
- * Reject the insert if it collides with an existing primary or unique single-field key value.
51
+ * Reject the insert if it collides with an existing primary or unique key value - a single field,
52
+ * or a composite of several for a multi-field key.
50
53
  * @param entity
51
54
  * @param record
52
55
  * @param entityValues
53
56
  */
54
57
  const assertUnique = async (entity, record, entityValues) => {
55
58
  for (const key of record.keys) {
56
- if (key.type !== 'primary' && key.type !== 'unique' || key.fields.length !== 1) {
59
+ if (key.type !== 'primary' && key.type !== 'unique') {
57
60
  continue
58
61
  }
59
- const isUnique = await (0, _keyUnique.keyUnique)(entity, key.fields, entityValues[key.fields[0]])
62
+ const checkKeys = key.fields.map(field => entityValues[field])
63
+ const isUnique = await (0, _keyUnique.keyUnique)(entity, key.fields, checkKeys)
60
64
  if (!isUnique) {
61
- throw new Error(`Duplicate value for ${key.type} key "${key.fields[0]}" on ${entity}`)
65
+ throw new Error(`Duplicate value for ${key.type} key "${key.fields.join(', ')}" on ${entity}`)
62
66
  }
63
67
  }
64
68
  }
@@ -104,9 +108,7 @@ const insertEntity = async (entity = '', values = [], dataSet = {}) => {
104
108
  record.entries.push(fileName)
105
109
  await writeFile(`${databasePath}${record.path}.json`, JSON.stringify(record, null, 2))
106
110
  for (const key of record.keys) {
107
- if (key.fields.length === 1) {
108
- await (0, _updateIndex.updateIndex)(record.path, key, entityValues[key.fields[0]], fileName)
109
- }
111
+ await (0, _updateIndex.updateIndex)(record.path, key, (0, _computeKeyValue.computeKeyValue)(key.fields, entityValues), fileName)
110
112
  }
111
113
  dataSet[entity].push(entityValues)
112
114
  }
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.insertEntity=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js");var _retrieveRecord=require("../utilities/retrieveRecord"),_updateIndex=require("../utilities/updateIndex"),_keyGenerate=require("../introspect/policies/keyGenerate"),_keyUnique=require("../introspect/policies/keyUnique"),_useDefault=require("../introspect/policies/useDefault"),_typeCheck=require("../introspect/policies/typeCheck"),_foreignKeyExists=require("../introspect/policies/foreignKeyExists"),_splitEntityProperty=require("../utilities/parsers/splitEntityProperty"),_config=require("../utilities/config");const{writeFile:writeFile}=require("fs/promises"),buildEntityValues=async(e,t,i={})=>{const r={};for(const s of t.definition){let t=i[s.name];if(s.autoGenerate?t=await(0,_keyGenerate.keyGenerate)(e,[s.name]):Object.prototype.hasOwnProperty.call(s,"default")&&(t=(0,_useDefault.useDefault)(s.default,t)),void 0!==t)r[s.name]=(0,_typeCheck.typeCheck)(s.type,t);else if(!s.optional)throw new Error(`Missing required field "${s.name}" for ${e}`)}return r},assertUnique=async(e,t,i)=>{for(const r of t.keys){if("primary"!==r.type&&"unique"!==r.type||1!==r.fields.length)continue;if(!await(0,_keyUnique.keyUnique)(e,r.fields,i[r.fields[0]]))throw new Error(`Duplicate value for ${r.type} key "${r.fields[0]}" on ${e}`)}},assertForeignKeys=async(e,t,i)=>{for(const r of t.keys){if("foreign"!==r.type||1!==r.fields.length||!r.references||1!==r.references.length)continue;const t=r.fields[0];if(!Object.prototype.hasOwnProperty.call(i,t))continue;const{entity:s,property:n}=(0,_splitEntityProperty.splitEntityProperty)(r.references[0].fields[0]);if(!await(0,_foreignKeyExists.foreignKeyExists)(s,n,i[t]))throw new Error(`Foreign key "${t}" on ${e} references a nonexistent ${s}.${n} = ${i[t]}`)}},insertEntity=async(e="",t=[],i={})=>{const r=(0,_config.getSetting)("databasePath","database/"),s=await(0,_retrieveRecord.retrieveRecord)(e),n=s.keys.find((e=>"primary"===e.type&&1===e.fields.length));Object.prototype.hasOwnProperty.call(i,e)||(i[e]=[]);for(const o of t){const t=await buildEntityValues(e,s,o);await assertUnique(e,s,t),await assertForeignKeys(e,s,t);const a=`${n?t[n.fields[0]]:crypto.randomUUID()}.json`;await writeFile(`${r}${s.path}/${a}`,JSON.stringify(t,null,2)),s.entries.push(a),await writeFile(`${r}${s.path}.json`,JSON.stringify(s,null,2));for(const e of s.keys)1===e.fields.length&&await(0,_updateIndex.updateIndex)(s.path,e,t[e.fields[0]],a);i[e].push(t)}return i};exports.insertEntity=insertEntity;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.insertEntity=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.map.js");var _retrieveRecord=require("../utilities/retrieveRecord"),_updateIndex=require("../utilities/updateIndex"),_keyGenerate=require("../introspect/policies/keyGenerate"),_keyUnique=require("../introspect/policies/keyUnique"),_useDefault=require("../introspect/policies/useDefault"),_typeCheck=require("../introspect/policies/typeCheck"),_foreignKeyExists=require("../introspect/policies/foreignKeyExists"),_splitEntityProperty=require("../utilities/parsers/splitEntityProperty"),_computeKeyValue=require("../utilities/computeKeyValue"),_config=require("../utilities/config");const{writeFile:writeFile}=require("fs/promises"),buildEntityValues=async(e,t,i={})=>{const r={};for(const s of t.definition){let t=i[s.name];if(s.autoGenerate?t=await(0,_keyGenerate.keyGenerate)(e,[s.name]):Object.prototype.hasOwnProperty.call(s,"default")&&(t=(0,_useDefault.useDefault)(s.default,t)),void 0!==t)r[s.name]=(0,_typeCheck.typeCheck)(s.type,t);else if(!s.optional)throw new Error(`Missing required field "${s.name}" for ${e}`)}return r},assertUnique=async(e,t,i)=>{for(const r of t.keys){if("primary"!==r.type&&"unique"!==r.type)continue;const t=r.fields.map((e=>i[e]));if(!await(0,_keyUnique.keyUnique)(e,r.fields,t))throw new Error(`Duplicate value for ${r.type} key "${r.fields.join(", ")}" on ${e}`)}},assertForeignKeys=async(e,t,i)=>{for(const r of t.keys){if("foreign"!==r.type||1!==r.fields.length||!r.references||1!==r.references.length)continue;const t=r.fields[0];if(!Object.prototype.hasOwnProperty.call(i,t))continue;const{entity:s,property:n}=(0,_splitEntityProperty.splitEntityProperty)(r.references[0].fields[0]);if(!await(0,_foreignKeyExists.foreignKeyExists)(s,n,i[t]))throw new Error(`Foreign key "${t}" on ${e} references a nonexistent ${s}.${n} = ${i[t]}`)}},insertEntity=async(e="",t=[],i={})=>{const r=(0,_config.getSetting)("databasePath","database/"),s=await(0,_retrieveRecord.retrieveRecord)(e),n=s.keys.find((e=>"primary"===e.type&&1===e.fields.length));Object.prototype.hasOwnProperty.call(i,e)||(i[e]=[]);for(const o of t){const t=await buildEntityValues(e,s,o);await assertUnique(e,s,t),await assertForeignKeys(e,s,t);const a=`${n?t[n.fields[0]]:crypto.randomUUID()}.json`;await writeFile(`${r}${s.path}/${a}`,JSON.stringify(t,null,2)),s.entries.push(a),await writeFile(`${r}${s.path}.json`,JSON.stringify(s,null,2));for(const e of s.keys)await(0,_updateIndex.updateIndex)(s.path,e,(0,_computeKeyValue.computeKeyValue)(e.fields,t),a);i[e].push(t)}return i};exports.insertEntity=insertEntity;
@@ -7,13 +7,17 @@ exports.updateEntity = void 0
7
7
  require('core-js/modules/es.json.stringify.js')
8
8
  require('core-js/modules/esnext.iterator.constructor.js')
9
9
  require('core-js/modules/esnext.iterator.find.js')
10
+ require('core-js/modules/esnext.iterator.map.js')
11
+ require('core-js/modules/esnext.iterator.some.js')
10
12
  const _retrieveRecord = require('../utilities/retrieveRecord')
11
13
  const _retrieveFile = require('../utilities/retrieveFile')
12
14
  const _updateIndex = require('../utilities/updateIndex')
13
15
  const _removeFromIndex = require('../utilities/removeFromIndex')
16
+ const _computeKeyValue = require('../utilities/computeKeyValue')
14
17
  const _keyUnique = require('../introspect/policies/keyUnique')
15
18
  const _typeCheck = require('../introspect/policies/typeCheck')
16
19
  const _foreignKeyExists = require('../introspect/policies/foreignKeyExists')
20
+ const _hasDependents = require('../introspect/policies/hasDependents')
17
21
  const _splitEntityProperty = require('../utilities/parsers/splitEntityProperty')
18
22
  const _config = require('../utilities/config')
19
23
  const {
@@ -21,53 +25,51 @@ const {
21
25
  } = require('fs/promises')
22
26
  /**
23
27
  * Same reasoning as deleteEntity: a record object carries no memory of which entry file it came
24
- * from, so re-scan entries[] and match by the record's own primary key value.
28
+ * from, so re-scan entries[] and match by the record's own primary key value(s) - a single field,
29
+ * or a composite of several for a multi-field primary key.
25
30
  * @param record
26
- * @param primaryField
31
+ * @param primaryKey
27
32
  * @param primaryValue
28
33
  */
29
- const findEntryFile = async (record, primaryField, primaryValue) => {
34
+ const findEntryFile = async (record, primaryKey, primaryValue) => {
30
35
  for (const fileName of record.entries) {
31
36
  const data = await (0, _retrieveFile.retrieveFile)(`${record.path}/${fileName}`)
32
- if (data[primaryField] == primaryValue) {
37
+ if ((0, _computeKeyValue.computeKeyValue)(primaryKey.fields, data) == primaryValue) {
33
38
  return fileName
34
39
  }
35
40
  }
36
41
  return null
37
42
  }
38
43
  /**
39
- * Only check uniqueness for a single-field primary/unique key whose value is actually changing -
40
- * checking an unchanged value would find the record's own existing entry and wrongly report it
41
- * as a duplicate of itself.
44
+ * True if the update actually changes at least one of this key's fields - a key none of whose
45
+ * fields appear in newValues, or whose given values are all unchanged, doesn't need re-checking.
46
+ * @param key
47
+ * @param oldData
48
+ * @param newValues
49
+ */
50
+ const keyFieldsChanged = (key, oldData, newValues) => key.fields.some(field => Object.prototype.hasOwnProperty.call(newValues, field) && newValues[field] != oldData[field])
51
+ /**
52
+ * Only check uniqueness for a primary/unique key (single-field, or composite) whose value is
53
+ * actually changing - checking an unchanged value would find the record's own existing entry and
54
+ * wrongly report it as a duplicate of itself.
42
55
  * @param entity
43
56
  * @param record
44
57
  * @param oldData
58
+ * @param newData
45
59
  * @param newValues
46
60
  */
47
- const assertUniqueForUpdate = async (entity, record, oldData, newValues) => {
61
+ const assertUniqueForUpdate = async (entity, record, oldData, newData, newValues) => {
48
62
  for (const key of record.keys) {
49
- if (key.type !== 'primary' && key.type !== 'unique' || key.fields.length !== 1) {
63
+ if (key.type !== 'primary' && key.type !== 'unique' || !keyFieldsChanged(key, oldData, newValues)) {
50
64
  continue
51
65
  }
52
- const field = key.fields[0]
53
- if (!Object.prototype.hasOwnProperty.call(newValues, field) || newValues[field] == oldData[field]) {
54
- continue
55
- }
56
- const isUnique = await (0, _keyUnique.keyUnique)(entity, key.fields, newValues[field])
66
+ const checkKeys = key.fields.map(field => newData[field])
67
+ const isUnique = await (0, _keyUnique.keyUnique)(entity, key.fields, checkKeys)
57
68
  if (!isUnique) {
58
- throw new Error(`Duplicate value for ${key.type} key "${field}" on ${entity}`)
69
+ throw new Error(`Duplicate value for ${key.type} key "${key.fields.join(', ')}" on ${entity}`)
59
70
  }
60
71
  }
61
72
  }
62
- /**
63
- * Move a single-field key's index entry from the old value to the new one wherever the update
64
- * actually changed that field.
65
- * @param record
66
- * @param oldData
67
- * @param newData
68
- * @param newValues
69
- * @param fileName
70
- */
71
73
  /**
72
74
  * Same "only if it actually changed" scoping as assertUniqueForUpdate: re-checking an unchanged
73
75
  * foreign key value would re-validate a reference this update never touches, which could fail an
@@ -96,17 +98,41 @@ const assertForeignKeysForUpdate = async (entity, record, oldData, newValues) =>
96
98
  }
97
99
  }
98
100
  }
101
+ /**
102
+ * RESTRICT: reject an update that would change a single-field primary key's value while another
103
+ * entity's foreign key still points at the old one - that reference would otherwise be left
104
+ * dangling, since the row itself isn't gone, just re-keyed. Only meaningful for a single-field
105
+ * primary key, matching foreign key support's own single-field-only scope.
106
+ * @param entity
107
+ * @param primaryKey
108
+ * @param oldData
109
+ * @param newValues
110
+ */
111
+ const assertNoDependentsForUpdate = async (entity, primaryKey, oldData, newValues) => {
112
+ if (!(0, _config.getSetting)('enforceForeignKeys', true) || primaryKey.fields.length !== 1 || !keyFieldsChanged(primaryKey, oldData, newValues)) {
113
+ return
114
+ }
115
+ const property = primaryKey.fields[0]
116
+ if (await (0, _hasDependents.hasDependents)(entity, property, oldData[property])) {
117
+ throw new Error(`Cannot update "${entity}" where "${property}" = ${oldData[property]} - other records still reference it`)
118
+ }
119
+ }
120
+ /**
121
+ * Move a key's index entry (single-field, or a composite for a multi-field key) from the old
122
+ * value to the new one wherever the update actually changed one of its fields.
123
+ * @param record
124
+ * @param oldData
125
+ * @param newData
126
+ * @param newValues
127
+ * @param fileName
128
+ */
99
129
  const reconcileIndexes = async (record, oldData, newData, newValues, fileName) => {
100
130
  for (const key of record.keys) {
101
- if (key.fields.length !== 1) {
131
+ if (!keyFieldsChanged(key, oldData, newValues)) {
102
132
  continue
103
133
  }
104
- const field = key.fields[0]
105
- if (!Object.prototype.hasOwnProperty.call(newValues, field) || newValues[field] == oldData[field]) {
106
- continue
107
- }
108
- await (0, _removeFromIndex.removeFromIndex)(record.path, key, oldData[field], fileName)
109
- await (0, _updateIndex.updateIndex)(record.path, key, newData[field], fileName)
134
+ await (0, _removeFromIndex.removeFromIndex)(record.path, key, (0, _computeKeyValue.computeKeyValue)(key.fields, oldData), fileName)
135
+ await (0, _updateIndex.updateIndex)(record.path, key, (0, _computeKeyValue.computeKeyValue)(key.fields, newData), fileName)
110
136
  }
111
137
  }
112
138
  const assertRequiredFields = (entity, record, newData) => {
@@ -136,23 +162,23 @@ const applyFieldTypes = (record, values) => {
136
162
  const updateEntity = async (entity = '', values = {}, dataSet = {}) => {
137
163
  const databasePath = (0, _config.getSetting)('databasePath', 'database/')
138
164
  const record = await (0, _retrieveRecord.retrieveRecord)(entity)
139
- const primaryKey = record.keys.find(key => key.type === 'primary' && key.fields.length === 1)
165
+ const primaryKey = record.keys.find(key => key.type === 'primary')
140
166
  if (!primaryKey) {
141
- throw new Error(`Cannot update "${entity}" without a single-field primary key`)
167
+ throw new Error(`Cannot update "${entity}" without a primary key`)
142
168
  }
143
- const primaryField = primaryKey.fields[0]
144
169
  const typedValues = applyFieldTypes(record, values)
145
170
  const toUpdate = dataSet[entity] || []
146
171
  const updatedRows = []
147
172
  for (const row of toUpdate) {
148
- const fileName = await findEntryFile(record, primaryField, row[primaryField])
173
+ const fileName = await findEntryFile(record, primaryKey, (0, _computeKeyValue.computeKeyValue)(primaryKey.fields, row))
149
174
  if (!fileName) {
150
- throw new Error(`Could not find entry for ${entity} where "${primaryField}" = ${row[primaryField]}`)
175
+ throw new Error(`Could not find entry for ${entity} where "${primaryKey.fields.join(', ')}" = ${(0, _computeKeyValue.computeKeyValue)(primaryKey.fields, row)}`)
151
176
  }
152
177
  const oldData = await (0, _retrieveFile.retrieveFile)(`${record.path}/${fileName}`)
153
- await assertUniqueForUpdate(entity, record, oldData, typedValues)
154
- await assertForeignKeysForUpdate(entity, record, oldData, typedValues)
155
178
  const newData = Object.assign({}, oldData, typedValues)
179
+ await assertUniqueForUpdate(entity, record, oldData, newData, typedValues)
180
+ await assertForeignKeysForUpdate(entity, record, oldData, typedValues)
181
+ await assertNoDependentsForUpdate(entity, primaryKey, oldData, typedValues)
156
182
  assertRequiredFields(entity, record, newData)
157
183
  await reconcileIndexes(record, oldData, newData, typedValues, fileName)
158
184
  await writeFile(`${databasePath}${record.path}/${fileName}`, JSON.stringify(newData, null, 2))
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.updateEntity=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js");var _retrieveRecord=require("../utilities/retrieveRecord"),_retrieveFile=require("../utilities/retrieveFile"),_updateIndex=require("../utilities/updateIndex"),_removeFromIndex=require("../utilities/removeFromIndex"),_keyUnique=require("../introspect/policies/keyUnique"),_typeCheck=require("../introspect/policies/typeCheck"),_foreignKeyExists=require("../introspect/policies/foreignKeyExists"),_splitEntityProperty=require("../utilities/parsers/splitEntityProperty"),_config=require("../utilities/config");const{writeFile:writeFile}=require("fs/promises"),findEntryFile=async(e,t,r)=>{for(const i of e.entries){if((await(0,_retrieveFile.retrieveFile)(`${e.path}/${i}`))[t]==r)return i}return null},assertUniqueForUpdate=async(e,t,r,i)=>{for(const n of t.keys){if("primary"!==n.type&&"unique"!==n.type||1!==n.fields.length)continue;const t=n.fields[0];if(!Object.prototype.hasOwnProperty.call(i,t)||i[t]==r[t])continue;if(!await(0,_keyUnique.keyUnique)(e,n.fields,i[t]))throw new Error(`Duplicate value for ${n.type} key "${t}" on ${e}`)}},assertForeignKeysForUpdate=async(e,t,r,i)=>{for(const n of t.keys){if("foreign"!==n.type||1!==n.fields.length||!n.references||1!==n.references.length)continue;const t=n.fields[0];if(!Object.prototype.hasOwnProperty.call(i,t)||i[t]==r[t])continue;const{entity:o,property:s}=(0,_splitEntityProperty.splitEntityProperty)(n.references[0].fields[0]);if(!await(0,_foreignKeyExists.foreignKeyExists)(o,s,i[t]))throw new Error(`Foreign key "${t}" on ${e} references a nonexistent ${o}.${s} = ${i[t]}`)}},reconcileIndexes=async(e,t,r,i,n)=>{for(const o of e.keys){if(1!==o.fields.length)continue;const s=o.fields[0];Object.prototype.hasOwnProperty.call(i,s)&&i[s]!=t[s]&&(await(0,_removeFromIndex.removeFromIndex)(e.path,o,t[s],n),await(0,_updateIndex.updateIndex)(e.path,o,r[s],n))}},assertRequiredFields=(e,t,r)=>{for(const i of t.definition)if(!i.optional&&void 0===r[i.name])throw new Error(`Missing required field "${i.name}" for ${e}`)},applyFieldTypes=(e,t)=>{const r=Object.assign({},t);for(const t of e.definition)Object.prototype.hasOwnProperty.call(r,t.name)&&(r[t.name]=(0,_typeCheck.typeCheck)(t.type,r[t.name]));return r},updateEntity=async(e="",t={},r={})=>{const i=(0,_config.getSetting)("databasePath","database/"),n=await(0,_retrieveRecord.retrieveRecord)(e),o=n.keys.find((e=>"primary"===e.type&&1===e.fields.length));if(!o)throw new Error(`Cannot update "${e}" without a single-field primary key`);const s=o.fields[0],a=applyFieldTypes(n,t),p=r[e]||[],c=[];for(const t of p){const r=await findEntryFile(n,s,t[s]);if(!r)throw new Error(`Could not find entry for ${e} where "${s}" = ${t[s]}`);const o=await(0,_retrieveFile.retrieveFile)(`${n.path}/${r}`);await assertUniqueForUpdate(e,n,o,a),await assertForeignKeysForUpdate(e,n,o,a);const p=Object.assign({},o,a);assertRequiredFields(e,n,p),await reconcileIndexes(n,o,p,a,r),await writeFile(`${i}${n.path}/${r}`,JSON.stringify(p,null,2)),c.push(p)}return r[e]=c,r};exports.updateEntity=updateEntity;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.updateEntity=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.find.js"),require("core-js/modules/esnext.iterator.map.js"),require("core-js/modules/esnext.iterator.some.js");var _retrieveRecord=require("../utilities/retrieveRecord"),_retrieveFile=require("../utilities/retrieveFile"),_updateIndex=require("../utilities/updateIndex"),_removeFromIndex=require("../utilities/removeFromIndex"),_computeKeyValue=require("../utilities/computeKeyValue"),_keyUnique=require("../introspect/policies/keyUnique"),_typeCheck=require("../introspect/policies/typeCheck"),_foreignKeyExists=require("../introspect/policies/foreignKeyExists"),_hasDependents=require("../introspect/policies/hasDependents"),_splitEntityProperty=require("../utilities/parsers/splitEntityProperty"),_config=require("../utilities/config");const{writeFile:writeFile}=require("fs/promises"),findEntryFile=async(e,t,r)=>{for(const i of e.entries){const s=await(0,_retrieveFile.retrieveFile)(`${e.path}/${i}`);if((0,_computeKeyValue.computeKeyValue)(t.fields,s)==r)return i}return null},keyFieldsChanged=(e,t,r)=>e.fields.some((e=>Object.prototype.hasOwnProperty.call(r,e)&&r[e]!=t[e])),assertUniqueForUpdate=async(e,t,r,i,s)=>{for(const o of t.keys){if("primary"!==o.type&&"unique"!==o.type||!keyFieldsChanged(o,r,s))continue;const t=o.fields.map((e=>i[e]));if(!await(0,_keyUnique.keyUnique)(e,o.fields,t))throw new Error(`Duplicate value for ${o.type} key "${o.fields.join(", ")}" on ${e}`)}},assertForeignKeysForUpdate=async(e,t,r,i)=>{for(const s of t.keys){if("foreign"!==s.type||1!==s.fields.length||!s.references||1!==s.references.length)continue;const t=s.fields[0];if(!Object.prototype.hasOwnProperty.call(i,t)||i[t]==r[t])continue;const{entity:o,property:n}=(0,_splitEntityProperty.splitEntityProperty)(s.references[0].fields[0]);if(!await(0,_foreignKeyExists.foreignKeyExists)(o,n,i[t]))throw new Error(`Foreign key "${t}" on ${e} references a nonexistent ${o}.${n} = ${i[t]}`)}},assertNoDependentsForUpdate=async(e,t,r,i)=>{if(!(0,_config.getSetting)("enforceForeignKeys",!0)||1!==t.fields.length||!keyFieldsChanged(t,r,i))return;const s=t.fields[0];if(await(0,_hasDependents.hasDependents)(e,s,r[s]))throw new Error(`Cannot update "${e}" where "${s}" = ${r[s]} - other records still reference it`)},reconcileIndexes=async(e,t,r,i,s)=>{for(const o of e.keys)keyFieldsChanged(o,t,i)&&(await(0,_removeFromIndex.removeFromIndex)(e.path,o,(0,_computeKeyValue.computeKeyValue)(o.fields,t),s),await(0,_updateIndex.updateIndex)(e.path,o,(0,_computeKeyValue.computeKeyValue)(o.fields,r),s))},assertRequiredFields=(e,t,r)=>{for(const i of t.definition)if(!i.optional&&void 0===r[i.name])throw new Error(`Missing required field "${i.name}" for ${e}`)},applyFieldTypes=(e,t)=>{const r=Object.assign({},t);for(const t of e.definition)Object.prototype.hasOwnProperty.call(r,t.name)&&(r[t.name]=(0,_typeCheck.typeCheck)(t.type,r[t.name]));return r},updateEntity=async(e="",t={},r={})=>{const i=(0,_config.getSetting)("databasePath","database/"),s=await(0,_retrieveRecord.retrieveRecord)(e),o=s.keys.find((e=>"primary"===e.type));if(!o)throw new Error(`Cannot update "${e}" without a primary key`);const n=applyFieldTypes(s,t),a=r[e]||[],p=[];for(const t of a){const r=await findEntryFile(s,o,(0,_computeKeyValue.computeKeyValue)(o.fields,t));if(!r)throw new Error(`Could not find entry for ${e} where "${o.fields.join(", ")}" = ${(0,_computeKeyValue.computeKeyValue)(o.fields,t)}`);const a=await(0,_retrieveFile.retrieveFile)(`${s.path}/${r}`),u=Object.assign({},a,n);await assertUniqueForUpdate(e,s,a,u,n),await assertForeignKeysForUpdate(e,s,a,n),await assertNoDependentsForUpdate(e,o,a,n),assertRequiredFields(e,s,u),await reconcileIndexes(s,a,u,n,r),await writeFile(`${i}${s.path}/${r}`,JSON.stringify(u,null,2)),p.push(u)}return r[e]=p,r};exports.updateEntity=updateEntity;
@@ -0,0 +1,13 @@
1
+ import { fieldName } from '../introspect/definitions/field';
2
+ /**
3
+ * The value a key (of any field count) indexes and checks uniqueness by. A single-field key uses
4
+ * the field's own raw value directly - this is the pre-existing, already-shipped index file
5
+ * format (e.g. a plain "2024-06-02" or 1), and stays exactly as it was for backward compatibility
6
+ * with real fixture data. A multi-field key has no single value to use, so its fields' values are
7
+ * combined into one deterministic composite (a JSON array), in the same field order as key.fields
8
+ * - two records only share a composite value if every field matches.
9
+ * @param fields
10
+ * @param values
11
+ */
12
+ export declare const computeKeyValue: (fields: fieldName[], values: Object) => any;
13
+ //# sourceMappingURL=computeKeyValue.d.ts.map
@@ -0,0 +1,26 @@
1
+ 'use strict'
2
+
3
+ Object.defineProperty(exports, '__esModule', {
4
+ value: true
5
+ })
6
+ exports.computeKeyValue = void 0
7
+ require('core-js/modules/es.json.stringify.js')
8
+ require('core-js/modules/esnext.iterator.constructor.js')
9
+ require('core-js/modules/esnext.iterator.map.js')
10
+ /**
11
+ * The value a key (of any field count) indexes and checks uniqueness by. A single-field key uses
12
+ * the field's own raw value directly - this is the pre-existing, already-shipped index file
13
+ * format (e.g. a plain "2024-06-02" or 1), and stays exactly as it was for backward compatibility
14
+ * with real fixture data. A multi-field key has no single value to use, so its fields' values are
15
+ * combined into one deterministic composite (a JSON array), in the same field order as key.fields
16
+ * - two records only share a composite value if every field matches.
17
+ * @param fields
18
+ * @param values
19
+ */
20
+ const computeKeyValue = (fields, values) => {
21
+ if (fields.length === 1) {
22
+ return values[fields[0]]
23
+ }
24
+ return JSON.stringify(fields.map(field => values[field]))
25
+ }
26
+ exports.computeKeyValue = computeKeyValue
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.computeKeyValue=void 0,require("core-js/modules/es.json.stringify.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.map.js");const computeKeyValue=(e,r)=>1===e.length?r[e[0]]:JSON.stringify(e.map((e=>r[e])));exports.computeKeyValue=computeKeyValue;
@@ -1,12 +1,18 @@
1
1
  export type RuntimeSettings = {
2
2
  databasePath: string;
3
3
  relativePath: string;
4
+ enforceForeignKeys: boolean;
4
5
  };
5
6
  /**
6
7
  * Set where this package's data lives: databasePath (a filesystem path, used in Node) and/or
7
8
  * relativePath (a URL prefix prepended to databasePath, used when reading over fetch() in a
8
9
  * browser). Call this once before using create()/drop()/query() - a fresh consumer has no other
9
10
  * way to point this package at their own data directory.
11
+ *
12
+ * enforceForeignKeys (default true) controls whether deleteEntity/updateEntity block an operation
13
+ * that would leave another record's foreign key pointing at a value that no longer exists -
14
+ * mirroring MySQL's own foreign_key_checks setting, which is also toggleable rather than always
15
+ * on, since strict enforcement isn't always what every consumer wants.
10
16
  * @param newSettings
11
17
  */
12
18
  export declare const configure: (newSettings: Partial<RuntimeSettings>) => void;
@@ -6,7 +6,8 @@ Object.defineProperty(exports, '__esModule', {
6
6
  exports.getSetting = exports.configure = void 0
7
7
  const defaults = {
8
8
  databasePath: 'database/',
9
- relativePath: ''
9
+ relativePath: '',
10
+ enforceForeignKeys: true
10
11
  }
11
12
  let settings = {
12
13
  ...defaults
@@ -16,6 +17,11 @@ let settings = {
16
17
  * relativePath (a URL prefix prepended to databasePath, used when reading over fetch() in a
17
18
  * browser). Call this once before using create()/drop()/query() - a fresh consumer has no other
18
19
  * way to point this package at their own data directory.
20
+ *
21
+ * enforceForeignKeys (default true) controls whether deleteEntity/updateEntity block an operation
22
+ * that would leave another record's foreign key pointing at a value that no longer exists -
23
+ * mirroring MySQL's own foreign_key_checks setting, which is also toggleable rather than always
24
+ * on, since strict enforcement isn't always what every consumer wants.
19
25
  * @param newSettings
20
26
  */
21
27
  const configure = newSettings => {
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getSetting=exports.configure=void 0;const defaults={databasePath:"database/",relativePath:""};let settings={...defaults};const configure=t=>{settings={...settings,...t}};exports.configure=configure;const getSetting=(t,e)=>void 0!==settings[t]?settings[t]:e;exports.getSetting=getSetting;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getSetting=exports.configure=void 0;const defaults={databasePath:"database/",relativePath:"",enforceForeignKeys:!0};let settings={...defaults};const configure=t=>{settings={...settings,...t}};exports.configure=configure;const getSetting=(t,e)=>void 0!==settings[t]?settings[t]:e;exports.getSetting=getSetting;