dyna-record 0.1.5 → 0.2.1

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.
Files changed (112) hide show
  1. package/README.md +11 -13
  2. package/dist/src/DynaRecord.d.ts +36 -28
  3. package/dist/src/DynaRecord.d.ts.map +1 -1
  4. package/dist/src/DynaRecord.js +39 -36
  5. package/dist/src/decorators/Table.d.ts.map +1 -1
  6. package/dist/src/decorators/attributes/BooleanAttribute.d.ts.map +1 -1
  7. package/dist/src/decorators/attributes/DateAttribute.d.ts.map +1 -1
  8. package/dist/src/decorators/attributes/EnumAttribute.d.ts.map +1 -1
  9. package/dist/src/decorators/attributes/ForeignKeyAttribute.d.ts.map +1 -1
  10. package/dist/src/decorators/attributes/NumberAttribute.d.ts.map +1 -1
  11. package/dist/src/decorators/attributes/PartitionKeyAttribute.d.ts.map +1 -1
  12. package/dist/src/decorators/attributes/SortKeyAttribute.d.ts.map +1 -1
  13. package/dist/src/decorators/attributes/StringAttribute.d.ts.map +1 -1
  14. package/dist/src/decorators/attributes/serializers.d.ts +1 -1
  15. package/dist/src/decorators/relationships/BelongsTo.d.ts.map +1 -1
  16. package/dist/src/decorators/relationships/HasAndBelongsToMany.d.ts.map +1 -1
  17. package/dist/src/decorators/relationships/HasMany.d.ts +7 -0
  18. package/dist/src/decorators/relationships/HasMany.d.ts.map +1 -1
  19. package/dist/src/decorators/relationships/HasMany.js +12 -2
  20. package/dist/src/decorators/relationships/HasOne.d.ts.map +1 -1
  21. package/dist/src/decorators/types.d.ts +2 -2
  22. package/dist/src/decorators/types.d.ts.map +1 -1
  23. package/dist/src/dynamo-utils/TransactGetBuilder.d.ts +4 -0
  24. package/dist/src/dynamo-utils/TransactGetBuilder.d.ts.map +1 -1
  25. package/dist/src/dynamo-utils/TransactGetBuilder.js +6 -0
  26. package/dist/src/metadata/EntityMetadata.d.ts +21 -1
  27. package/dist/src/metadata/EntityMetadata.d.ts.map +1 -1
  28. package/dist/src/metadata/EntityMetadata.js +33 -0
  29. package/dist/src/metadata/TableMetadata.d.ts.map +1 -1
  30. package/dist/src/metadata/TableMetadata.js +1 -3
  31. package/dist/src/metadata/relationship-metadata/BelongsToRelationship.d.ts +1 -1
  32. package/dist/src/metadata/relationship-metadata/BelongsToRelationship.js +1 -1
  33. package/dist/src/metadata/relationship-metadata/HasManyRelationship.d.ts +2 -1
  34. package/dist/src/metadata/relationship-metadata/HasManyRelationship.d.ts.map +1 -1
  35. package/dist/src/metadata/relationship-metadata/HasManyRelationship.js +2 -1
  36. package/dist/src/metadata/relationship-metadata/OwnedByRelationship.d.ts +18 -0
  37. package/dist/src/metadata/relationship-metadata/OwnedByRelationship.d.ts.map +1 -0
  38. package/dist/src/metadata/relationship-metadata/OwnedByRelationship.js +26 -0
  39. package/dist/src/metadata/relationship-metadata/RelationshipMetadata.d.ts +1 -1
  40. package/dist/src/metadata/relationship-metadata/RelationshipMetadata.d.ts.map +1 -1
  41. package/dist/src/metadata/relationship-metadata/index.d.ts +2 -1
  42. package/dist/src/metadata/relationship-metadata/index.d.ts.map +1 -1
  43. package/dist/src/metadata/relationship-metadata/index.js +3 -1
  44. package/dist/src/metadata/relationship-metadata/types.d.ts +7 -2
  45. package/dist/src/metadata/relationship-metadata/types.d.ts.map +1 -1
  46. package/dist/src/metadata/relationship-metadata/utils.d.ts.map +1 -1
  47. package/dist/src/metadata/relationship-metadata/utils.js +3 -0
  48. package/dist/src/metadata/types.d.ts +7 -4
  49. package/dist/src/metadata/types.d.ts.map +1 -1
  50. package/dist/src/metadata/utils.d.ts +8 -4
  51. package/dist/src/metadata/utils.d.ts.map +1 -1
  52. package/dist/src/metadata/utils.js +8 -1
  53. package/dist/src/operations/Create/Create.d.ts +95 -16
  54. package/dist/src/operations/Create/Create.d.ts.map +1 -1
  55. package/dist/src/operations/Create/Create.js +171 -22
  56. package/dist/src/operations/Delete/Delete.d.ts +32 -23
  57. package/dist/src/operations/Delete/Delete.d.ts.map +1 -1
  58. package/dist/src/operations/Delete/Delete.js +117 -100
  59. package/dist/src/operations/Delete/types.d.ts +0 -3
  60. package/dist/src/operations/Delete/types.d.ts.map +1 -1
  61. package/dist/src/operations/FindById/FindById.d.ts +7 -28
  62. package/dist/src/operations/FindById/FindById.d.ts.map +1 -1
  63. package/dist/src/operations/FindById/FindById.js +36 -125
  64. package/dist/src/operations/FindById/types.d.ts +8 -9
  65. package/dist/src/operations/FindById/types.d.ts.map +1 -1
  66. package/dist/src/operations/Query/Query.d.ts +2 -1
  67. package/dist/src/operations/Query/Query.d.ts.map +1 -1
  68. package/dist/src/operations/Query/Query.js +17 -4
  69. package/dist/src/operations/Query/types.d.ts +39 -5
  70. package/dist/src/operations/Query/types.d.ts.map +1 -1
  71. package/dist/src/operations/Update/Update.d.ts +185 -24
  72. package/dist/src/operations/Update/Update.d.ts.map +1 -1
  73. package/dist/src/operations/Update/Update.js +409 -72
  74. package/dist/src/operations/Update/UpdateDryRun.d.ts +10 -0
  75. package/dist/src/operations/Update/UpdateDryRun.d.ts.map +1 -0
  76. package/dist/src/operations/Update/UpdateDryRun.js +15 -0
  77. package/dist/src/operations/Update/index.d.ts +1 -0
  78. package/dist/src/operations/Update/index.d.ts.map +1 -1
  79. package/dist/src/operations/Update/index.js +3 -1
  80. package/dist/src/operations/types.d.ts +6 -1
  81. package/dist/src/operations/types.d.ts.map +1 -1
  82. package/dist/src/operations/utils/expressionBuilder.d.ts.map +1 -1
  83. package/dist/src/operations/utils/expressionBuilder.js +1 -4
  84. package/dist/src/operations/utils/index.d.ts +0 -1
  85. package/dist/src/operations/utils/index.d.ts.map +1 -1
  86. package/dist/src/operations/utils/index.js +0 -6
  87. package/dist/src/operations/utils/utils.d.ts +13 -3
  88. package/dist/src/operations/utils/utils.d.ts.map +1 -1
  89. package/dist/src/operations/utils/utils.js +33 -3
  90. package/dist/src/query-utils/Filters.d.ts +1 -1
  91. package/dist/src/query-utils/Filters.d.ts.map +1 -1
  92. package/dist/src/query-utils/Filters.js +7 -15
  93. package/dist/src/query-utils/QueryBuilder.d.ts.map +1 -1
  94. package/dist/src/query-utils/QueryBuilder.js +9 -1
  95. package/dist/src/relationships/JoinTable.d.ts +17 -7
  96. package/dist/src/relationships/JoinTable.d.ts.map +1 -1
  97. package/dist/src/relationships/JoinTable.js +77 -20
  98. package/dist/src/relationships/index.d.ts +0 -1
  99. package/dist/src/relationships/index.d.ts.map +1 -1
  100. package/dist/src/relationships/index.js +1 -3
  101. package/dist/src/types.d.ts +6 -8
  102. package/dist/src/types.d.ts.map +1 -1
  103. package/dist/src/utils.d.ts +10 -32
  104. package/dist/src/utils.d.ts.map +1 -1
  105. package/dist/src/utils.js +13 -59
  106. package/package.json +5 -5
  107. package/dist/src/operations/utils/RelationshipTransactions.d.ts +0 -64
  108. package/dist/src/operations/utils/RelationshipTransactions.d.ts.map +0 -1
  109. package/dist/src/operations/utils/RelationshipTransactions.js +0 -125
  110. package/dist/src/relationships/BelongsToLink.d.ts +0 -51
  111. package/dist/src/relationships/BelongsToLink.d.ts.map +0 -1
  112. package/dist/src/relationships/BelongsToLink.js +0 -57
@@ -8,12 +8,22 @@ const dynamo_utils_1 = require("../../dynamo-utils");
8
8
  const utils_1 = require("../../utils");
9
9
  const OperationBase_1 = __importDefault(require("../OperationBase"));
10
10
  const utils_2 = require("../utils");
11
+ const utils_3 = require("../../metadata/utils");
11
12
  /**
12
- * Represents the operation for creating a new entity in the database, including handling its attributes and any related entities' associations. It will handle de-normalizing data to support relationships
13
+ * Represents an operation to create a new entity record in DynamoDB, including all necessary
14
+ * denormalized relationship records. This ensures that "BelongsTo" and "HasMany" relationships
15
+ * are properly maintained at the time of entity creation.
13
16
  *
14
- * It encapsulates the logic required to translate entity attributes to a format suitable for DynamoDB, execute the creation transaction, and manage any relationships defined by the entity, such as "BelongsTo" or "HasMany" links.
17
+ * **What it does:**
18
+ * - Converts the given attributes into a DynamoDB-compatible format.
19
+ * - Inserts a new entity record, ensuring no duplicate primary key conflicts.
20
+ * - For each "BelongsTo" relationship that includes a foreign key:
21
+ * - Verifies the referenced entity exists.
22
+ * - Creates a denormalized link record in the related entity's partition.
23
+ * - If the entity's relationships imply additional denormalized records in its own partition,
24
+ * those are also created after verifying the related entities exist.
15
25
  *
16
- * Only attributes defined on the model can be configured, and will be enforced via types and runtime schema validation.
26
+ * Only attributes defined on the entity model can be set, validated both at compile-time and runtime.
17
27
  *
18
28
  * @template T - The type of the entity being created, extending `DynaRecord`.
19
29
  */
@@ -23,11 +33,22 @@ class Create extends OperationBase_1.default {
23
33
  super(Entity);
24
34
  this.#transactionBuilder = new dynamo_utils_1.TransactWriteBuilder();
25
35
  }
26
- // TODO I need to handle the error where this throws because an item already exists and throw a better error...
27
36
  /**
28
- * Create an entity transaction, including relationship transactions (EX: Creating BelongsToLinks for HasMany, checking existence of relationships, etc)
29
- * @param attributes
30
- * @returns
37
+ * Executes the create operation.
38
+ *
39
+ * **What it does:**
40
+ * - Parses and validates the provided attributes against the entity schema.
41
+ * - Generates any required reserved attributes (like `id`, `createdAt`, and `updatedAt`).
42
+ * - Inserts the new entity record into DynamoDB, ensuring it doesn't already exist.
43
+ * - For each defined "BelongsTo" relationship, ensures the related entity exists and creates
44
+ * a corresponding denormalized "link" record.
45
+ * - If the entity's creation implies that related records must also be denormalized into its own
46
+ * partition (due to "BelongsTo" links), retrieves and inserts those link records.
47
+ *
48
+ * @param attributes - Attributes to initialize the new entity. Must be defined on the model and valid per schema constraints.
49
+ * @returns A promise that resolves to the newly created entity with all attributes, including automatically set fields.
50
+ * @throws If the entity already exists, a uniqueness violation error is raised.
51
+ * @throws If a required foreign key does not correspond to an existing entity, an error is raised.
31
52
  */
32
53
  async run(attributes) {
33
54
  const entityAttrs = this.entityMetadata.parseRawEntityDefinedAttributes(attributes);
@@ -35,18 +56,30 @@ class Create extends OperationBase_1.default {
35
56
  const entityData = { ...reservedAttrs, ...entityAttrs };
36
57
  const tableItem = (0, utils_1.entityToTableItem)(this.EntityClass, entityData);
37
58
  this.buildPutItemTransaction(tableItem, entityData.id);
38
- await this.buildRelationshipTransactions(entityData);
59
+ this.buildBelongsToTransactions(entityData, tableItem);
60
+ // Attempt to fetch all belongs-to entities to properly create reverse denormalization links
61
+ const belongsToTableItems = await this.getBelongsToTableItems(entityData);
62
+ // If there are any belongs-to relationships, add the inverse link records into the new entity's partition
63
+ if (belongsToTableItems.length > 0) {
64
+ this.buildAddBelongsToLinkToSelfTransactions(reservedAttrs.id, belongsToTableItems);
65
+ }
39
66
  await this.#transactionBuilder.executeTransaction();
40
67
  return (0, utils_1.tableItemToEntity)(this.EntityClass, tableItem);
41
68
  }
42
69
  /**
43
- * Builds the entity attributes
44
- * @param attributes
45
- * @returns
70
+ * Builds and returns entity attributes that must be reserved for system usage.
71
+ *
72
+ * **What it does:**
73
+ * - Generates a unique entity ID if the entity's schema does not specify an `id` field.
74
+ * - Sets `createdAt` and `updatedAt` to the current time.
75
+ * - Builds the partition and sort key values based on the entity's class and generated ID.
76
+ *
77
+ * @param entityAttrs - The user-provided entity attributes.
78
+ * @returns The combined attributes including all reserved fields.
79
+ * @private
46
80
  */
47
81
  buildReservedAttributes(entityAttrs) {
48
82
  const { idField } = this.entityMetadata;
49
- // If the entity has has a custom id field use that, otherwise generate a uuid
50
83
  const id = idField === undefined
51
84
  ? (0, uuid_1.v4)()
52
85
  : entityAttrs[idField];
@@ -66,28 +99,144 @@ class Create extends OperationBase_1.default {
66
99
  return { ...keys, ...defaultAttrs };
67
100
  }
68
101
  /**
69
- * Build the transaction for the parent entity Create item request
70
- * @param tableItem
102
+ * Adds a "PutItem" transaction for the new entity record.
103
+ *
104
+ * **What it does:**
105
+ * - Ensures the primary key does not already exist, preventing duplication.
106
+ *
107
+ * @param tableItem - The DynamoDB table item for the entity to put.
108
+ * @param id - The unique identifier of the new entity.
109
+ * @private
71
110
  */
72
111
  buildPutItemTransaction(tableItem, id) {
73
112
  const { name: tableName } = this.tableMetadata;
74
113
  const putExpression = {
75
114
  TableName: tableName,
76
115
  Item: tableItem,
77
- ConditionExpression: `attribute_not_exists(${this.partitionKeyAlias})` // Ensure item doesn't already exist
116
+ ConditionExpression: `attribute_not_exists(${this.partitionKeyAlias})`
78
117
  };
79
118
  this.#transactionBuilder.addPut(putExpression, `${this.EntityClass.name} with id: ${id} already exists`);
80
119
  }
81
120
  /**
82
- * Build transaction items for associations
83
- * @param entityData
121
+ * Adds "PutItem" transactions to create denormalized "BelongsTo" link records in the related entity's partitions.
122
+ *
123
+ * **What it does:**
124
+ * - For each "BelongsTo" relationship with a defined foreign key, checks that the related entity exists.
125
+ * - Inserts a "link" item into the related entity's partition to maintain denormalized relationships.
126
+ *
127
+ * @param entityData - The complete set of entity attributes for the new entity.
128
+ * @param tableItem - The main entity's DynamoDB table item.
129
+ * @private
84
130
  */
85
- async buildRelationshipTransactions(entityData) {
86
- const relationshipTransactions = new utils_2.RelationshipTransactions({
87
- Entity: this.EntityClass,
88
- transactionBuilder: this.#transactionBuilder
131
+ buildBelongsToTransactions(entityData, tableItem) {
132
+ const tableName = this.tableMetadata.name;
133
+ const relMetadata = this.entityMetadata.belongsToOrOwnedByRelationships;
134
+ for (const relMeta of relMetadata) {
135
+ const foreignKey = (0, utils_2.extractForeignKeyFromEntity)(relMeta, entityData);
136
+ if (foreignKey !== undefined) {
137
+ // Ensure referenced entity exists before linking
138
+ this.buildRelationshipExistsConditionTransaction(relMeta, foreignKey);
139
+ const key = (0, utils_2.buildBelongsToLinkKey)(this.EntityClass, entityData.id, relMeta, foreignKey);
140
+ this.#transactionBuilder.addPut({
141
+ TableName: tableName,
142
+ Item: { ...tableItem, ...key },
143
+ ConditionExpression: `attribute_not_exists(${this.partitionKeyAlias})`
144
+ }, `${relMeta.target.name} with id: ${foreignKey} already has an associated ${this.EntityClass.name}`);
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Retrieves the DynamoDB items for all entities that the new entity references via "BelongsTo" relationships.
150
+ *
151
+ * **What it does:**
152
+ * - For each "BelongsTo" relationship, queries DynamoDB for the related entity record.
153
+ * - Returns all found related items as an array.
154
+ * - If no relationships or no foreign keys are present, returns an empty array.
155
+ *
156
+ * @param entityData - The attributes of the entity being created.
157
+ * @returns A promise that resolves to an array of related DynamoDB items.
158
+ * @private
159
+ */
160
+ async getBelongsToTableItems(entityData) {
161
+ const { name: tableName } = this.tableMetadata;
162
+ const transactionBuilder = new dynamo_utils_1.TransactGetBuilder();
163
+ const relMetas = this.entityMetadata.relationships;
164
+ const belongsToRelMetas = Object.values(relMetas).filter(relMeta => (0, utils_3.isBelongsToRelationship)(relMeta));
165
+ belongsToRelMetas.forEach(relMeta => {
166
+ const fk = (0, utils_2.extractForeignKeyFromEntity)(relMeta, entityData);
167
+ if (fk !== undefined) {
168
+ transactionBuilder.addGet({
169
+ TableName: tableName,
170
+ Key: {
171
+ [this.partitionKeyAlias]: relMeta.target.partitionKeyValue(fk),
172
+ [this.sortKeyAlias]: relMeta.target.name
173
+ }
174
+ });
175
+ }
176
+ });
177
+ if (transactionBuilder.hasTransactions()) {
178
+ const results = await transactionBuilder.executeTransaction();
179
+ return results.reduce((acc, res) => {
180
+ if (res.Item !== undefined)
181
+ acc.push(res.Item);
182
+ return acc;
183
+ }, []);
184
+ }
185
+ return [];
186
+ }
187
+ /**
188
+ * Adds a condition check transaction to ensure that the entity referenced by a "BelongsTo" foreign key exists.
189
+ *
190
+ * **What it does:**
191
+ * - Checks the existence of the related entity before creating the link item.
192
+ * - If the related entity does not exist, the transaction will fail, preventing creation of dangling references.
193
+ *
194
+ * @param rel - The "BelongsTo" relationship metadata.
195
+ * @param relationshipId - The foreign key value referencing the related entity.
196
+ * @private
197
+ */
198
+ buildRelationshipExistsConditionTransaction(rel, relationshipId) {
199
+ const { name: tableName } = this.tableMetadata;
200
+ const errMsg = `${rel.target.name} with ID '${relationshipId}' does not exist`;
201
+ const conditionCheck = {
202
+ TableName: tableName,
203
+ Key: {
204
+ [this.partitionKeyAlias]: rel.target.partitionKeyValue(relationshipId),
205
+ [this.sortKeyAlias]: rel.target.name
206
+ },
207
+ ConditionExpression: `attribute_exists(${this.partitionKeyAlias})`
208
+ };
209
+ this.#transactionBuilder.addConditionCheck(conditionCheck, errMsg);
210
+ }
211
+ /**
212
+ * For each related entity referenced by a "BelongsTo" relationship, insert a denormalized copy of that entity
213
+ * into the new entity's partition. This maintains a consistent, denormalized view of relationships.
214
+ *
215
+ * **What it does:**
216
+ * - Adds "PutItem" operations to create link records in the newly created entity's partition.
217
+ * - Ensures these link records don't already exist.
218
+ *
219
+ * @param entityId - The newly created entity's ID.
220
+ * @param belongsToTableItems - The table items representing each related "BelongsTo" entity.
221
+ * @private
222
+ */
223
+ buildAddBelongsToLinkToSelfTransactions(entityId, belongsToTableItems) {
224
+ const pk = this.EntityClass.partitionKeyValue(entityId);
225
+ const typeAlias = this.tableMetadata.defaultAttributes.type.alias;
226
+ belongsToTableItems.forEach(tableItem => {
227
+ const relationshipType = tableItem[typeAlias];
228
+ const key = {
229
+ [this.partitionKeyAlias]: pk,
230
+ [this.sortKeyAlias]: relationshipType
231
+ };
232
+ this.#transactionBuilder.addPut({
233
+ TableName: this.tableMetadata.name,
234
+ Item: { ...tableItem, ...key },
235
+ ConditionExpression: `attribute_not_exists(${this.partitionKeyAlias})`
236
+ },
237
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
238
+ `${this.EntityClass.name} already has an associated ${relationshipType}`);
89
239
  });
90
- await relationshipTransactions.build(entityData);
91
240
  }
92
241
  }
93
242
  exports.default = Create;
@@ -1,10 +1,10 @@
1
- import DynaRecord from "../../DynaRecord";
1
+ import type DynaRecord from "../../DynaRecord";
2
2
  import type { EntityClass } from "../../types";
3
3
  import OperationBase from "../OperationBase";
4
4
  /**
5
5
  * Implements the operation for deleting an entity and its related data from the database within the ORM framework.
6
6
  *
7
- * Delete an entity, everything in its partition, BelongsToLinks and nullifies ForeignKeys on attributes that BelongTo it
7
+ * Delete an entity, everything in its partition, denormalized records and nullifies ForeignKeys on attributes that BelongTo it
8
8
  * If the foreign key is non nullable than it will throw a NullConstraintViolationError
9
9
  *
10
10
  * The `Delete` operation supports complex scenarios, such as deleting related entities in "BelongsTo" relationships, nullifying or removing foreign keys to maintain data integrity, and handling many-to-many relationships through join tables.
@@ -18,21 +18,38 @@ declare class Delete<T extends DynaRecord> extends OperationBase<T> {
18
18
  * Delete an item by id
19
19
  * - Deletes the given entity
20
20
  * - Deletes each item in the entity's partition
21
- * - For each item in the entity's partition which is of type 'BelongsToLink' it:
21
+ * - For each item in the entity's partition which is a denormalized record it:
22
22
  * - Will nullify the associated relationship's ForeignKey attribute if the attribute is nullable
23
23
  * @param id
24
24
  */
25
25
  run(id: string): Promise<void>;
26
26
  /**
27
- * Deletes an item
28
- * @param item
27
+ * Prefetch the item being deleted including items in its partition from denormalized associated records
28
+ * @param id
29
+ * @returns - The item and its associated links denormalized
30
+ */
31
+ private preFetch;
32
+ /**
33
+ * Returns true if the linked entity needs to have a foreign key nullified
34
+ * @param relMeta
35
+ * @returns
36
+ */
37
+ private doesEntityNeedForeignKeyNullified;
38
+ /**
39
+ * Delete the entity and denormalized links from BelongsTo relationships
40
+ * @param self
41
+ */
42
+ private buildDeleteSelfTransactions;
43
+ /**
44
+ * Deletes an item when given the table keys
45
+ * @param keys - The key to delete representing the table keys, as opposed to the entities keys
29
46
  */
30
47
  private buildDeleteItemTransaction;
31
48
  /**
32
- * If the item has a JoinTable entry (is part of HasAndBelongsToMany relationship) then delete both JoinTable entries
33
- * @param item - BelongsToLink from HasAndBelongsToMany relationship
49
+ * Deletes an item when given the keys of the entity. Converts the keys to the table alias
50
+ * @param keys - The key to delete representing the entity keys, as opposed to the table keys
34
51
  */
35
- private buildDeleteJoinTableLinkTransaction;
52
+ private buildDeleteEntityTransaction;
36
53
  /**
37
54
  * If the item being deleted has a foreign key reference, nullify the associated relationship's ForeignKey attribute
38
55
  * If the ForeignKey is non nullable than it throws a NullConstraintViolationError
@@ -46,29 +63,21 @@ declare class Delete<T extends DynaRecord> extends OperationBase<T> {
46
63
  */
47
64
  private buildDeleteAssociatedBelongsTransaction;
48
65
  /**
49
- * Deletes associated BelongsToLink for a BelongsTo HasMany relationship
50
- * @param relMeta
51
- * @param entityId
52
- * @param foreignKeyValue
53
- */
54
- private buildDeleteBelongsToHasManyTransaction;
55
- /**
56
- * Deletes associated BelongsToLink for a BelongsTo HasOne relationship
57
- * @param relMeta
58
- * @param foreignKeyValue
66
+ * If the item has a JoinTable entry (is part of HasAndBelongsToMany relationship) then delete both JoinTable entries
67
+ * @param item - Denormalized record from HasAndBelongsToMany relationship
59
68
  */
60
- private buildDeleteBelongsToHasOneTransaction;
69
+ private buildDeleteJoinTableLinkTransaction;
61
70
  /**
62
- * Type guard to check if the item being evaluated is the currentClass
63
- * @param item
64
- * @returns
71
+ * If the entity being deleted is owned by another entity via a unidirectional relationship, delete the denormalized records
72
+ * @param self - The entity being deleted
65
73
  */
66
- private isEntityClass;
74
+ private buildDeleteOwnedByRelationships;
67
75
  /**
68
76
  * Track validation errors and throw AggregateError after all validations have been run
69
77
  * @param err
70
78
  */
71
79
  private trackValidationError;
80
+ private buildKeyObject;
72
81
  }
73
82
  export default Delete;
74
83
  //# sourceMappingURL=Delete.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Delete.d.ts","sourceRoot":"","sources":["../../../../src/operations/Delete/Delete.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,kBAAkB,CAAC;AAc1C,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,aAAa,CAAC;AAEnE,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAI7C;;;;;;;;;GASG;AACH,cAAM,MAAM,CAAC,CAAC,SAAS,UAAU,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;;gBAS7C,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAiBlC;;;;;;;OAOG;IACU,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyC3C;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAmBlC;;;OAGG;IACH,OAAO,CAAC,mCAAmC;IAoB3C;;;;OAIG;IACH,OAAO,CAAC,iCAAiC;IA0CzC;;;;OAIG;IACH,OAAO,CAAC,uCAAuC;IA0B/C;;;;;OAKG;IACH,OAAO,CAAC,sCAAsC;IAkB9C;;;;OAIG;IACH,OAAO,CAAC,qCAAqC;IAiB7C;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;CAG7B;AAED,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Delete.d.ts","sourceRoot":"","sources":["../../../../src/operations/Delete/Delete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAC;AAY/C,OAAO,KAAK,EAEV,WAAW,EAEZ,MAAM,aAAa,CAAC;AAErB,OAAO,aAAa,MAAM,kBAAkB,CAAC;AA2B7C;;;;;;;;;GASG;AACH,cAAM,MAAM,CAAC,CAAC,SAAS,UAAU,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;;gBAS7C,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAiBlC;;;;;;;OAOG;IACU,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC3C;;;;OAIG;YACW,QAAQ;IA+BtB;;;;OAIG;IACH,OAAO,CAAC,iCAAiC;IASzC;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAOnC;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAalC;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAkBpC;;;;OAIG;YACW,iCAAiC;IA2B/C;;;;OAIG;IACH,OAAO,CAAC,uCAAuC;IA2B/C;;;OAGG;IACH,OAAO,CAAC,mCAAmC;IAiB3C;;;OAGG;IACH,OAAO,CAAC,+BAA+B;IAqBvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,cAAc;CAcvB;AAED,eAAe,MAAM,CAAC"}
@@ -3,19 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const DynaRecord_1 = __importDefault(require("../../DynaRecord"));
7
6
  const dynamo_utils_1 = require("../../dynamo-utils");
8
7
  const errors_1 = require("../../errors");
9
8
  const metadata_1 = __importDefault(require("../../metadata"));
10
9
  const utils_1 = require("../../metadata/utils");
11
- const relationships_1 = require("../../relationships");
12
10
  const utils_2 = require("../../utils");
13
11
  const OperationBase_1 = __importDefault(require("../OperationBase"));
12
+ const Update_1 = require("../Update");
14
13
  const utils_3 = require("../utils");
15
14
  /**
16
15
  * Implements the operation for deleting an entity and its related data from the database within the ORM framework.
17
16
  *
18
- * Delete an entity, everything in its partition, BelongsToLinks and nullifies ForeignKeys on attributes that BelongTo it
17
+ * Delete an entity, everything in its partition, denormalized records and nullifies ForeignKeys on attributes that BelongTo it
19
18
  * If the foreign key is non nullable than it will throw a NullConstraintViolationError
20
19
  *
21
20
  * The `Delete` operation supports complex scenarios, such as deleting related entities in "BelongsTo" relationships, nullifying or removing foreign keys to maintain data integrity, and handling many-to-many relationships through join tables.
@@ -45,33 +44,23 @@ class Delete extends OperationBase_1.default {
45
44
  * Delete an item by id
46
45
  * - Deletes the given entity
47
46
  * - Deletes each item in the entity's partition
48
- * - For each item in the entity's partition which is of type 'BelongsToLink' it:
47
+ * - For each item in the entity's partition which is a denormalized record it:
49
48
  * - Will nullify the associated relationship's ForeignKey attribute if the attribute is nullable
50
49
  * @param id
51
50
  */
52
51
  async run(id) {
53
- const items = await this.EntityClass.query(id);
54
- if (items.length === 0) {
55
- throw new errors_1.NotFoundError(`Item does not exist: ${id}`);
56
- }
57
- for (const item of items) {
58
- if (item.id === id && this.isEntityClass(item)) {
59
- this.buildDeleteItemTransaction(item, {
60
- errorMessage: `Failed to delete ${this.EntityClass.name} with Id: ${id}`
61
- });
62
- this.buildDeleteAssociatedBelongsTransaction(id, item);
63
- }
64
- if (item instanceof relationships_1.BelongsToLink) {
65
- this.buildDeleteItemTransaction(item, {
66
- errorMessage: `Failed to delete BelongsToLink with keys: ${JSON.stringify({
67
- [this.#partitionKeyField]: item[this.#partitionKeyField],
68
- [this.#sortKeyField]: item[this.#sortKeyField]
69
- })}`
70
- });
71
- this.buildNullifyForeignKeyTransaction(item);
72
- this.buildDeleteJoinTableLinkTransaction(item);
73
- }
74
- }
52
+ const preFetchRes = await this.preFetch(id);
53
+ this.buildDeleteSelfTransactions(preFetchRes.self);
54
+ preFetchRes.linkedEntities.forEach(item => {
55
+ this.buildDeleteEntityTransaction(item, {
56
+ errorMessage: `Failed to delete denormalized record with keys: ${JSON.stringify(this.buildKeyObject(item, this.#partitionKeyField, this.#sortKeyField))}`
57
+ });
58
+ this.buildDeleteJoinTableLinkTransaction(item);
59
+ });
60
+ this.buildDeleteOwnedByRelationships(preFetchRes.self);
61
+ await Promise.all(preFetchRes.linkedEntitiesWithFkRef.map(async (entity) => {
62
+ await this.buildNullifyForeignKeyTransaction(entity);
63
+ }));
75
64
  if (this.#validationErrors.length === 0) {
76
65
  await this.#transactionBuilder.executeTransaction();
77
66
  }
@@ -80,35 +69,75 @@ class Delete extends OperationBase_1.default {
80
69
  }
81
70
  }
82
71
  /**
83
- * Deletes an item
84
- * @param item
72
+ * Prefetch the item being deleted including items in its partition from denormalized associated records
73
+ * @param id
74
+ * @returns - The item and its associated links denormalized
75
+ */
76
+ async preFetch(id) {
77
+ const items = await this.EntityClass.query(id);
78
+ const prefetchResult = items.reduce((acc, item) => {
79
+ const isItemSelf = item.id === id && item instanceof this.EntityClass;
80
+ if (isItemSelf) {
81
+ acc.self = item;
82
+ }
83
+ else if (this.doesEntityNeedForeignKeyNullified(item)) {
84
+ acc.linkedEntitiesWithFkRef.push(item);
85
+ }
86
+ else {
87
+ acc.linkedEntities.push(item);
88
+ }
89
+ return acc;
90
+ }, { linkedEntities: [], linkedEntitiesWithFkRef: [] });
91
+ if (prefetchResult.self === undefined) {
92
+ throw new errors_1.NotFoundError(`Item does not exist: ${id}`);
93
+ }
94
+ return {
95
+ self: prefetchResult.self,
96
+ linkedEntities: prefetchResult.linkedEntities,
97
+ linkedEntitiesWithFkRef: prefetchResult.linkedEntitiesWithFkRef
98
+ };
99
+ }
100
+ /**
101
+ * Returns true if the linked entity needs to have a foreign key nullified
102
+ * @param relMeta
103
+ * @returns
104
+ */
105
+ doesEntityNeedForeignKeyNullified(linkedEntity) {
106
+ const relMeta = this.#relationsLookup[linkedEntity.type];
107
+ return (!(0, utils_1.isBelongsToRelationship)(relMeta) &&
108
+ (0, utils_1.isRelationshipMetadataWithForeignKey)(relMeta));
109
+ }
110
+ /**
111
+ * Delete the entity and denormalized links from BelongsTo relationships
112
+ * @param self
113
+ */
114
+ buildDeleteSelfTransactions(self) {
115
+ this.buildDeleteEntityTransaction(self, {
116
+ errorMessage: `Failed to delete ${this.EntityClass.name} with Id: ${self.id}`
117
+ });
118
+ this.buildDeleteAssociatedBelongsTransaction(self.id, self);
119
+ }
120
+ /**
121
+ * Deletes an item when given the table keys
122
+ * @param keys - The key to delete representing the table keys, as opposed to the entities keys
85
123
  */
86
- buildDeleteItemTransaction(item, options) {
87
- const pkField = this.#partitionKeyField;
88
- const skField = this.#sortKeyField;
124
+ buildDeleteItemTransaction(keys, options) {
89
125
  this.#transactionBuilder.addDelete({
90
126
  TableName: this.#tableName,
91
- Key: {
92
- [this.partitionKeyAlias]: item[pkField],
93
- [this.sortKeyAlias]: item[skField]
94
- }
127
+ Key: keys
95
128
  }, options.errorMessage);
96
129
  }
97
130
  /**
98
- * If the item has a JoinTable entry (is part of HasAndBelongsToMany relationship) then delete both JoinTable entries
99
- * @param item - BelongsToLink from HasAndBelongsToMany relationship
131
+ * Deletes an item when given the keys of the entity. Converts the keys to the table alias
132
+ * @param keys - The key to delete representing the entity keys, as opposed to the table keys
100
133
  */
101
- buildDeleteJoinTableLinkTransaction(item) {
102
- const relMeta = this.#relationsLookup[item.foreignEntityType];
103
- if ((0, utils_1.isHasAndBelongsToManyRelationship)(relMeta)) {
104
- // Inverse the keys to delete the other JoinTable entry
105
- const belongsToLinksKeys = {
106
- [this.#partitionKeyField]: item[this.#sortKeyField],
107
- [this.#sortKeyField]: item[this.#partitionKeyField]
108
- };
109
- this.buildDeleteItemTransaction(belongsToLinksKeys, {
110
- errorMessage: `Failed to delete BelongsToLink with keys: ${JSON.stringify(belongsToLinksKeys)}`
111
- });
134
+ buildDeleteEntityTransaction(keys, options) {
135
+ if ((0, utils_2.isKeyOfObject)(keys, this.#partitionKeyField) &&
136
+ (0, utils_2.isKeyOfObject)(keys, this.#sortKeyField)) {
137
+ this.buildDeleteItemTransaction({
138
+ [this.partitionKeyAlias]: keys[this.#partitionKeyField],
139
+ [this.sortKeyAlias]: keys[this.#sortKeyField]
140
+ }, options);
112
141
  }
113
142
  }
114
143
  /**
@@ -116,28 +145,18 @@ class Delete extends OperationBase_1.default {
116
145
  * If the ForeignKey is non nullable than it throws a NullConstraintViolationError
117
146
  * @param item
118
147
  */
119
- buildNullifyForeignKeyTransaction(item) {
120
- const relMeta = this.#relationsLookup[item.foreignEntityType];
148
+ async buildNullifyForeignKeyTransaction(item) {
149
+ const relMeta = this.#relationsLookup[item.type];
121
150
  if ((0, utils_1.isRelationshipMetadataWithForeignKey)(relMeta)) {
122
151
  const entityAttrs = metadata_1.default.getEntityAttributes(relMeta.target.name);
123
152
  const attrMeta = Object.values(entityAttrs).find(attr => attr.name === relMeta.foreignKey);
124
153
  if (attrMeta?.nullable === false) {
125
154
  this.trackValidationError(new errors_1.NullConstraintViolationError(`Cannot set ${relMeta.target.name} with id: '${item.id}' attribute '${relMeta.foreignKey}' to null`));
126
155
  }
127
- const tableKeys = (0, utils_2.entityToTableItem)(this.EntityClass, {
128
- [this.#partitionKeyField]: relMeta.target.partitionKeyValue(item.foreignKey),
129
- [this.#sortKeyField]: relMeta.target.name
130
- });
131
- const tableAttrs = (0, utils_2.entityToTableItem)(relMeta.target, {
132
- [relMeta.foreignKey]: null
133
- });
134
- const expression = (0, utils_3.expressionBuilder)(tableAttrs);
135
- this.#transactionBuilder.addUpdate({
136
- TableName: this.#tableName,
137
- Key: tableKeys,
138
- UpdateExpression: expression.UpdateExpression,
139
- ExpressionAttributeNames: expression.ExpressionAttributeNames
140
- }, `Failed to remove foreign key attribute from ${relMeta.target.name} with Id: ${item.foreignKey}`);
156
+ else {
157
+ const op = new Update_1.UpdateDryRun(relMeta.target, this.#transactionBuilder);
158
+ await op.run(item.id, { [relMeta.foreignKey]: null });
159
+ }
141
160
  }
142
161
  }
143
162
  /**
@@ -150,52 +169,41 @@ class Delete extends OperationBase_1.default {
150
169
  if ((0, utils_2.isKeyOfObject)(item, relMeta.foreignKey) &&
151
170
  item[relMeta.foreignKey] !== undefined) {
152
171
  const foreignKeyValue = item[relMeta.foreignKey];
153
- if ((0, utils_1.doesEntityBelongToRelAsHasMany)(this.EntityClass, relMeta)) {
154
- this.buildDeleteBelongsToHasManyTransaction(relMeta, entityId, foreignKeyValue);
155
- }
156
- if ((0, utils_1.doesEntityBelongToRelAsHasOne)(this.EntityClass, relMeta)) {
157
- this.buildDeleteBelongsToHasOneTransaction(relMeta, foreignKeyValue);
158
- }
172
+ const belongsToLinksKeys = (0, utils_3.buildBelongsToLinkKey)(this.EntityClass, entityId, relMeta, foreignKeyValue);
173
+ this.buildDeleteItemTransaction(belongsToLinksKeys, {
174
+ errorMessage: `Failed to delete denormalized record with keys: ${JSON.stringify(belongsToLinksKeys)}`
175
+ });
159
176
  }
160
177
  });
161
178
  }
162
179
  /**
163
- * Deletes associated BelongsToLink for a BelongsTo HasMany relationship
164
- * @param relMeta
165
- * @param entityId
166
- * @param foreignKeyValue
180
+ * If the item has a JoinTable entry (is part of HasAndBelongsToMany relationship) then delete both JoinTable entries
181
+ * @param item - Denormalized record from HasAndBelongsToMany relationship
167
182
  */
168
- buildDeleteBelongsToHasManyTransaction(relMeta, entityId, foreignKeyValue) {
169
- const belongsToLinksKeys = {
170
- [this.#partitionKeyField]: relMeta.target.partitionKeyValue(foreignKeyValue),
171
- [this.#sortKeyField]: this.EntityClass.partitionKeyValue(entityId)
172
- };
173
- this.buildDeleteItemTransaction(belongsToLinksKeys, {
174
- errorMessage: `Failed to delete BelongsToLink with keys: ${JSON.stringify(belongsToLinksKeys)}`
175
- });
183
+ buildDeleteJoinTableLinkTransaction(item) {
184
+ const relMeta = this.#relationsLookup[item.type];
185
+ if ((0, utils_1.isHasAndBelongsToManyRelationship)(relMeta)) {
186
+ const belongsToLinksKeys = this.buildKeyObject(item, this.#sortKeyField, this.#partitionKeyField);
187
+ this.buildDeleteEntityTransaction(belongsToLinksKeys, {
188
+ errorMessage: `Failed to delete denormalized record with keys: ${JSON.stringify(belongsToLinksKeys)}`
189
+ });
190
+ }
176
191
  }
177
192
  /**
178
- * Deletes associated BelongsToLink for a BelongsTo HasOne relationship
179
- * @param relMeta
180
- * @param foreignKeyValue
193
+ * If the entity being deleted is owned by another entity via a unidirectional relationship, delete the denormalized records
194
+ * @param self - The entity being deleted
181
195
  */
182
- buildDeleteBelongsToHasOneTransaction(relMeta, foreignKeyValue) {
183
- const belongsToLinksKeys = {
184
- [this.#partitionKeyField]: relMeta.target.partitionKeyValue(foreignKeyValue),
185
- [this.#sortKeyField]: this.EntityClass.name
186
- };
187
- this.buildDeleteItemTransaction(belongsToLinksKeys, {
188
- errorMessage: `Failed to delete BelongsToLink with keys: ${JSON.stringify(belongsToLinksKeys)}`
196
+ buildDeleteOwnedByRelationships(self) {
197
+ this.entityMetadata.ownedByRelationships.forEach(ownedByRelMeta => {
198
+ if ((0, utils_2.isKeyOfObject)(self, ownedByRelMeta.foreignKey)) {
199
+ const fkValue = self[ownedByRelMeta.foreignKey];
200
+ const keys = (0, utils_3.buildBelongsToLinkKey)(this.EntityClass, self.id, ownedByRelMeta, fkValue);
201
+ this.buildDeleteItemTransaction(keys, {
202
+ errorMessage: `Failed to delete denormalized record with keys: ${JSON.stringify(keys)}`
203
+ });
204
+ }
189
205
  });
190
206
  }
191
- /**
192
- * Type guard to check if the item being evaluated is the currentClass
193
- * @param item
194
- * @returns
195
- */
196
- isEntityClass(item) {
197
- return item instanceof DynaRecord_1.default;
198
- }
199
207
  /**
200
208
  * Track validation errors and throw AggregateError after all validations have been run
201
209
  * @param err
@@ -203,5 +211,14 @@ class Delete extends OperationBase_1.default {
203
211
  trackValidationError(err) {
204
212
  this.#validationErrors.push(err);
205
213
  }
214
+ buildKeyObject(item, pkField, skField) {
215
+ if ((0, utils_2.isKeyOfObject)(item, pkField) && (0, utils_2.isKeyOfObject)(item, skField)) {
216
+ return {
217
+ [this.#partitionKeyField]: item[pkField],
218
+ [this.#sortKeyField]: item[skField]
219
+ };
220
+ }
221
+ throw new Error("Invalid keys");
222
+ }
206
223
  }
207
224
  exports.default = Delete;
@@ -1,7 +1,4 @@
1
- import type DynaRecord from "../../DynaRecord";
2
- import { type QueryResult } from "../Query";
3
1
  export interface DeleteOptions {
4
2
  errorMessage: string;
5
3
  }
6
- export type ItemKeys<T extends DynaRecord> = Partial<QueryResult<T>>;
7
4
  //# sourceMappingURL=types.d.ts.map