dyno-table 2.0.2 → 2.1.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.
package/dist/index.cjs CHANGED
@@ -3774,27 +3774,133 @@ function createEntityAwareDeleteBuilder(builder, entityName) {
3774
3774
  return createEntityAwareBuilder(builder, entityName);
3775
3775
  }
3776
3776
 
3777
- // src/entity.ts
3778
- function defineEntity(config) {
3779
- const entityTypeAttributeName = config.settings?.entityTypeAttributeName ?? "entityType";
3780
- const buildIndexes = (dataForKeyGeneration, table) => {
3781
- return Object.entries(config.indexes ?? {}).reduce(
3782
- (acc, [indexName, index]) => {
3783
- const key = index.generateKey(dataForKeyGeneration);
3784
- const gsiConfig = table.gsis[indexName];
3777
+ // src/entity/ddb-indexing.ts
3778
+ var IndexBuilder = class {
3779
+ /**
3780
+ * Creates a new IndexBuilder instance
3781
+ *
3782
+ * @param table - The DynamoDB table instance
3783
+ * @param indexes - The index definitions
3784
+ */
3785
+ constructor(table, indexes = {}) {
3786
+ this.table = table;
3787
+ this.indexes = indexes;
3788
+ }
3789
+ /**
3790
+ * Build index attributes for item creation
3791
+ *
3792
+ * @param item - The item to generate indexes for
3793
+ * @param options - Options for building indexes
3794
+ * @returns Record of GSI attribute names to their values
3795
+ */
3796
+ buildForCreate(item, options = {}) {
3797
+ const attributes = {};
3798
+ for (const [indexName, indexDef] of Object.entries(this.indexes)) {
3799
+ if (options.excludeReadOnly && indexDef.isReadOnly) {
3800
+ continue;
3801
+ }
3802
+ const key = indexDef.generateKey(item);
3803
+ const gsiConfig = this.table.gsis[indexName];
3804
+ if (!gsiConfig) {
3805
+ throw new Error(`GSI configuration not found for index: ${indexName}`);
3806
+ }
3807
+ if (key.pk) {
3808
+ attributes[gsiConfig.partitionKey] = key.pk;
3809
+ }
3810
+ if (key.sk && gsiConfig.sortKey) {
3811
+ attributes[gsiConfig.sortKey] = key.sk;
3812
+ }
3813
+ }
3814
+ return attributes;
3815
+ }
3816
+ /**
3817
+ * Build index attributes for item updates
3818
+ *
3819
+ * @param currentData - The current data before update
3820
+ * @param updates - The update data
3821
+ * @param options - Options for building indexes
3822
+ * @returns Record of GSI attribute names to their updated values
3823
+ */
3824
+ buildForUpdate(currentData, updates) {
3825
+ const attributes = {};
3826
+ const updatedItem = { ...currentData, ...updates };
3827
+ for (const [indexName, indexDef] of Object.entries(this.indexes)) {
3828
+ if (indexDef.isReadOnly) {
3829
+ continue;
3830
+ }
3831
+ let shouldUpdateIndex = false;
3832
+ try {
3833
+ const currentKey = indexDef.generateKey(currentData);
3834
+ const updatedKey = indexDef.generateKey(updatedItem);
3835
+ if (currentKey.pk !== updatedKey.pk || currentKey.sk !== updatedKey.sk) {
3836
+ shouldUpdateIndex = true;
3837
+ }
3838
+ } catch {
3839
+ shouldUpdateIndex = true;
3840
+ }
3841
+ if (!shouldUpdateIndex) {
3842
+ continue;
3843
+ }
3844
+ try {
3845
+ const key = indexDef.generateKey(updatedItem);
3846
+ if (this.hasUndefinedValues(key)) {
3847
+ throw new Error(
3848
+ `Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be marked as readOnly.`
3849
+ );
3850
+ }
3851
+ const gsiConfig = this.table.gsis[indexName];
3785
3852
  if (!gsiConfig) {
3786
3853
  throw new Error(`GSI configuration not found for index: ${indexName}`);
3787
3854
  }
3788
3855
  if (key.pk) {
3789
- acc[gsiConfig.partitionKey] = key.pk;
3856
+ attributes[gsiConfig.partitionKey] = key.pk;
3790
3857
  }
3791
3858
  if (key.sk && gsiConfig.sortKey) {
3792
- acc[gsiConfig.sortKey] = key.sk;
3859
+ attributes[gsiConfig.sortKey] = key.sk;
3793
3860
  }
3794
- return acc;
3795
- },
3796
- {}
3797
- );
3861
+ } catch (error) {
3862
+ if (error instanceof Error && error.message.includes("insufficient data")) {
3863
+ throw error;
3864
+ }
3865
+ throw new Error(
3866
+ `Cannot update entity: insufficient data to regenerate index "${indexName}". All attributes required by the index must be provided in the update operation, or the index must be readOnly.`
3867
+ );
3868
+ }
3869
+ }
3870
+ return attributes;
3871
+ }
3872
+ /**
3873
+ * Check if a key has undefined values
3874
+ *
3875
+ * @param key - The index key to check
3876
+ * @returns True if the key contains undefined values, false otherwise
3877
+ */
3878
+ hasUndefinedValues(key) {
3879
+ return (key.pk?.includes("undefined") ?? false) || (key.sk?.includes("undefined") ?? false);
3880
+ }
3881
+ };
3882
+
3883
+ // src/entity/index-utils.ts
3884
+ function buildIndexes(dataForKeyGeneration, table, indexes, excludeReadOnly = false) {
3885
+ if (!indexes) {
3886
+ return {};
3887
+ }
3888
+ const indexBuilder = new IndexBuilder(table, indexes);
3889
+ return indexBuilder.buildForCreate(dataForKeyGeneration, { excludeReadOnly });
3890
+ }
3891
+ function buildIndexUpdates(currentData, updates, table, indexes) {
3892
+ if (!indexes) {
3893
+ return {};
3894
+ }
3895
+ const indexBuilder = new IndexBuilder(table, indexes);
3896
+ return indexBuilder.buildForUpdate(currentData, updates);
3897
+ }
3898
+
3899
+ // src/entity/entity.ts
3900
+ function defineEntity(config) {
3901
+ const entityTypeAttributeName = config.settings?.entityTypeAttributeName ?? "entityType";
3902
+ const buildIndexes2 = (dataForKeyGeneration, table, excludeReadOnly = false) => {
3903
+ return buildIndexes(dataForKeyGeneration, table, config.indexes, excludeReadOnly);
3798
3904
  };
3799
3905
  const wrapMethodWithPreparation = (originalMethod, prepareFn, context) => {
3800
3906
  const wrappedMethod = (...args) => {
@@ -3846,7 +3952,7 @@ function defineEntity(config) {
3846
3952
  ...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
3847
3953
  };
3848
3954
  const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3849
- const indexes = buildIndexes(dataForKeyGeneration, table);
3955
+ const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
3850
3956
  const validatedItem = {
3851
3957
  ...dataForKeyGeneration,
3852
3958
  [entityTypeAttributeName]: config.name,
@@ -3872,7 +3978,7 @@ function defineEntity(config) {
3872
3978
  ...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
3873
3979
  };
3874
3980
  const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3875
- const indexes = buildIndexes(dataForKeyGeneration, table);
3981
+ const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
3876
3982
  const validatedItem = {
3877
3983
  ...dataForKeyGeneration,
3878
3984
  [entityTypeAttributeName]: config.name,
@@ -3914,7 +4020,7 @@ function defineEntity(config) {
3914
4020
  ...generateTimestamps(["createdAt", "updatedAt"], validatedData.value)
3915
4021
  };
3916
4022
  const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3917
- const indexes = buildIndexes(dataForKeyGeneration, table);
4023
+ const indexes = buildIndexes2(dataForKeyGeneration, table, false);
3918
4024
  const validatedItem = {
3919
4025
  [table.partitionKey]: primaryKey.pk,
3920
4026
  ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
@@ -3940,7 +4046,7 @@ function defineEntity(config) {
3940
4046
  ...generateTimestamps(["createdAt", "updatedAt"], validationResult.value)
3941
4047
  };
3942
4048
  const primaryKey = config.primaryKey.generateKey(dataForKeyGeneration);
3943
- const indexes = buildIndexes(dataForKeyGeneration, table);
4049
+ const indexes = buildIndexes(dataForKeyGeneration, table, config.indexes, false);
3944
4050
  const validatedItem = {
3945
4051
  [table.partitionKey]: primaryKey.pk,
3946
4052
  ...table.sortKey ? { [table.sortKey]: primaryKey.sk } : {},
@@ -3974,13 +4080,21 @@ function defineEntity(config) {
3974
4080
  }
3975
4081
  return createEntityAwarePutBuilder(builder, config.name);
3976
4082
  },
3977
- get: (key) => createEntityAwareGetBuilder(table.get(config.primaryKey.generateKey(key)), config.name),
4083
+ get: (key) => {
4084
+ return createEntityAwareGetBuilder(table.get(config.primaryKey.generateKey(key)), config.name);
4085
+ },
3978
4086
  update: (key, data) => {
3979
4087
  const primaryKeyObj = config.primaryKey.generateKey(key);
3980
4088
  const builder = table.update(primaryKeyObj);
3981
4089
  builder.condition(eq(entityTypeAttributeName, config.name));
3982
4090
  const timestamps = generateTimestamps(["updatedAt"], data);
3983
- builder.set({ ...data, ...timestamps });
4091
+ const indexUpdates = buildIndexUpdates(
4092
+ { ...key },
4093
+ { ...data, ...timestamps },
4094
+ table,
4095
+ config.indexes
4096
+ );
4097
+ builder.set({ ...data, ...timestamps, ...indexUpdates });
3984
4098
  return builder;
3985
4099
  },
3986
4100
  delete: (key) => {
@@ -4051,21 +4165,57 @@ function createQueries() {
4051
4165
  }
4052
4166
  function createIndex() {
4053
4167
  return {
4054
- input: (schema) => ({
4055
- partitionKey: (pkFn) => ({
4056
- sortKey: (skFn) => ({
4057
- name: "custom",
4058
- partitionKey: "pk",
4059
- sortKey: "sk",
4060
- generateKey: (item) => ({ pk: pkFn(item), sk: skFn(item) })
4168
+ input: (schema) => {
4169
+ const createIndexBuilder = (isReadOnly = false) => ({
4170
+ partitionKey: (pkFn) => ({
4171
+ sortKey: (skFn) => {
4172
+ const index = {
4173
+ name: "custom",
4174
+ partitionKey: "pk",
4175
+ sortKey: "sk",
4176
+ isReadOnly,
4177
+ generateKey: (item) => {
4178
+ const data = schema["~standard"].validate(item);
4179
+ if ("issues" in data && data.issues) {
4180
+ throw new Error(`Index validation failed: ${data.issues.map((i) => i.message).join(", ")}`);
4181
+ }
4182
+ const validData = "value" in data ? data.value : item;
4183
+ return { pk: pkFn(validData), sk: skFn(validData) };
4184
+ }
4185
+ };
4186
+ return Object.assign(index, {
4187
+ readOnly: (value = false) => ({
4188
+ ...index,
4189
+ isReadOnly: value
4190
+ })
4191
+ });
4192
+ },
4193
+ withoutSortKey: () => {
4194
+ const index = {
4195
+ name: "custom",
4196
+ partitionKey: "pk",
4197
+ isReadOnly,
4198
+ generateKey: (item) => {
4199
+ const data = schema["~standard"].validate(item);
4200
+ if ("issues" in data && data.issues) {
4201
+ throw new Error(`Index validation failed: ${data.issues.map((i) => i.message).join(", ")}`);
4202
+ }
4203
+ const validData = "value" in data ? data.value : item;
4204
+ return { pk: pkFn(validData) };
4205
+ }
4206
+ };
4207
+ return Object.assign(index, {
4208
+ readOnly: (value = true) => ({
4209
+ ...index,
4210
+ isReadOnly: value
4211
+ })
4212
+ });
4213
+ }
4061
4214
  }),
4062
- withoutSortKey: () => ({
4063
- name: "custom",
4064
- partitionKey: "pk",
4065
- generateKey: (item) => ({ pk: pkFn(item) })
4066
- })
4067
- })
4068
- })
4215
+ readOnly: (value = true) => createIndexBuilder(value)
4216
+ });
4217
+ return createIndexBuilder(false);
4218
+ }
4069
4219
  };
4070
4220
  }
4071
4221