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