s3db.js 4.0.2 → 4.1.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.
package/dist/s3db.es.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* istanbul ignore file */
2
2
  import { customAlphabet, urlAlphabet } from 'nanoid';
3
- import { chunk, merge, cloneDeep, isEmpty, invert, uniq, isString as isString$1, get as get$1, set, isFunction as isFunction$1 } from 'lodash-es';
3
+ import { chunk, merge, isEmpty, invert, uniq, cloneDeep, isString as isString$1, get as get$1, set, isFunction as isFunction$1 } from 'lodash-es';
4
4
  import { PromisePool } from '@supercharge/promise-pool';
5
5
  import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand, CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
6
6
  import { createHash } from 'crypto';
@@ -936,7 +936,7 @@ class Client extends EventEmitter {
936
936
  Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, key) : key
937
937
  };
938
938
  try {
939
- const response = await this.client.send(new HeadObjectCommand(options2));
939
+ const response = await this.sendCommand(new HeadObjectCommand(options2));
940
940
  this.emit("headObject", response, options2);
941
941
  return response;
942
942
  } catch (error) {
@@ -3480,9 +3480,10 @@ class Schema {
3480
3480
  this.attributes = attributes || {};
3481
3481
  this.passphrase = passphrase ?? "secret";
3482
3482
  this.options = merge({}, this.defaultOptions(), options);
3483
+ const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
3483
3484
  this.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
3484
3485
  { $$async: true },
3485
- cloneDeep(this.attributes)
3486
+ processedAttributes
3486
3487
  ));
3487
3488
  if (this.options.generateAutoHooks) this.generateAutoHooks();
3488
3489
  if (!isEmpty(map)) {
@@ -3563,6 +3564,7 @@ class Schema {
3563
3564
  version,
3564
3565
  attributes
3565
3566
  } = isString$1(data) ? JSON.parse(data) : data;
3567
+ attributes = Schema._importAttributes(attributes);
3566
3568
  const schema = new Schema({
3567
3569
  map,
3568
3570
  name,
@@ -3572,22 +3574,60 @@ class Schema {
3572
3574
  });
3573
3575
  return schema;
3574
3576
  }
3577
+ /**
3578
+ * Recursively import attributes, parsing only stringified objects (legacy)
3579
+ */
3580
+ static _importAttributes(attrs) {
3581
+ if (typeof attrs === "string") {
3582
+ try {
3583
+ const parsed = JSON.parse(attrs);
3584
+ if (typeof parsed === "object" && parsed !== null) {
3585
+ return Schema._importAttributes(parsed);
3586
+ }
3587
+ } catch (e) {
3588
+ }
3589
+ return attrs;
3590
+ }
3591
+ if (Array.isArray(attrs)) {
3592
+ return attrs.map((a) => Schema._importAttributes(a));
3593
+ }
3594
+ if (typeof attrs === "object" && attrs !== null) {
3595
+ const out = {};
3596
+ for (const [k, v] of Object.entries(attrs)) {
3597
+ out[k] = Schema._importAttributes(v);
3598
+ }
3599
+ return out;
3600
+ }
3601
+ return attrs;
3602
+ }
3575
3603
  export() {
3576
3604
  const data = {
3577
3605
  version: this.version,
3578
3606
  name: this.name,
3579
3607
  options: this.options,
3580
- attributes: cloneDeep(this.attributes),
3608
+ attributes: this._exportAttributes(this.attributes),
3581
3609
  map: this.map
3582
3610
  };
3583
- for (const [name, definition] of Object.entries(this.attributes)) {
3584
- if (typeof definition !== "string") {
3585
- data.attributes[name] = JSON.stringify(definition);
3586
- } else {
3587
- data.attributes[name] = definition;
3611
+ return data;
3612
+ }
3613
+ /**
3614
+ * Recursively export attributes, keeping objects as objects and only serializing leaves as string
3615
+ */
3616
+ _exportAttributes(attrs) {
3617
+ if (typeof attrs === "string") {
3618
+ return attrs;
3619
+ }
3620
+ if (Array.isArray(attrs)) {
3621
+ return attrs.map((a) => this._exportAttributes(a));
3622
+ }
3623
+ if (typeof attrs === "object" && attrs !== null) {
3624
+ const out = {};
3625
+ for (const [k, v] of Object.entries(attrs)) {
3626
+ out[k] = this._exportAttributes(v);
3588
3627
  }
3628
+ return out;
3589
3629
  }
3590
- return data;
3630
+ return attrs;
3591
3631
  }
3592
3632
  async applyHooksActions(resourceItem, hook) {
3593
3633
  for (const [attribute, actions] of Object.entries(this.options.hooks[hook])) {
@@ -3628,6 +3668,26 @@ class Schema {
3628
3668
  await this.applyHooksActions(rest, "afterUnmap");
3629
3669
  return unflatten(rest);
3630
3670
  }
3671
+ /**
3672
+ * Preprocess attributes to convert nested objects into validator-compatible format
3673
+ * @param {Object} attributes - Original attributes
3674
+ * @returns {Object} Processed attributes for validator
3675
+ */
3676
+ preprocessAttributesForValidation(attributes) {
3677
+ const processed = {};
3678
+ for (const [key, value] of Object.entries(attributes)) {
3679
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3680
+ processed[key] = {
3681
+ type: "object",
3682
+ properties: this.preprocessAttributesForValidation(value),
3683
+ strict: false
3684
+ };
3685
+ } else {
3686
+ processed[key] = value;
3687
+ }
3688
+ }
3689
+ return processed;
3690
+ }
3631
3691
  }
3632
3692
 
3633
3693
  var global$1 = (typeof global !== "undefined" ? global :
@@ -8943,7 +9003,7 @@ class Resource extends EventEmitter {
8943
9003
  continue;
8944
9004
  }
8945
9005
  for (const fieldName of Object.keys(partitionDef.fields)) {
8946
- if (!currentAttributes.includes(fieldName)) {
9006
+ if (!this.fieldExistsInAttributes(fieldName)) {
8947
9007
  throw new Error(
8948
9008
  `Partition '${partitionName}' uses field '${fieldName}' which does not exist in resource version '${this.version}'. Available fields: ${currentAttributes.join(", ")}. This version of resource does not have support for this partition.`
8949
9009
  );
@@ -8951,6 +9011,25 @@ class Resource extends EventEmitter {
8951
9011
  }
8952
9012
  }
8953
9013
  }
9014
+ /**
9015
+ * Check if a field (including nested fields) exists in the current attributes
9016
+ * @param {string} fieldName - Field name (can be nested like 'utm.source')
9017
+ * @returns {boolean} True if field exists
9018
+ */
9019
+ fieldExistsInAttributes(fieldName) {
9020
+ if (!fieldName.includes(".")) {
9021
+ return Object.keys(this.attributes || {}).includes(fieldName);
9022
+ }
9023
+ const keys = fieldName.split(".");
9024
+ let currentLevel = this.attributes || {};
9025
+ for (const key of keys) {
9026
+ if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
9027
+ return false;
9028
+ }
9029
+ currentLevel = currentLevel[key];
9030
+ }
9031
+ return true;
9032
+ }
8954
9033
  /**
8955
9034
  * Apply a single partition rule to a field value
8956
9035
  * @param {*} value - The field value
@@ -9013,17 +9092,38 @@ class Resource extends EventEmitter {
9013
9092
  const partitionSegments = [];
9014
9093
  const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
9015
9094
  for (const [fieldName, rule] of sortedFields) {
9016
- const fieldValue = this.applyPartitionRule(data[fieldName], rule);
9017
- if (fieldValue === void 0 || fieldValue === null) {
9095
+ const fieldValue = this.getNestedFieldValue(data, fieldName);
9096
+ const transformedValue = this.applyPartitionRule(fieldValue, rule);
9097
+ if (transformedValue === void 0 || transformedValue === null) {
9018
9098
  return null;
9019
9099
  }
9020
- partitionSegments.push(`${fieldName}=${fieldValue}`);
9100
+ partitionSegments.push(`${fieldName}=${transformedValue}`);
9021
9101
  }
9022
9102
  if (partitionSegments.length === 0) {
9023
9103
  return null;
9024
9104
  }
9025
9105
  return join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${id}`);
9026
9106
  }
9107
+ /**
9108
+ * Get nested field value from data object using dot notation
9109
+ * @param {Object} data - Data object
9110
+ * @param {string} fieldPath - Field path (e.g., "utm.source", "address.city")
9111
+ * @returns {*} Field value
9112
+ */
9113
+ getNestedFieldValue(data, fieldPath) {
9114
+ if (!fieldPath.includes(".")) {
9115
+ return data[fieldPath];
9116
+ }
9117
+ const keys = fieldPath.split(".");
9118
+ let value = data;
9119
+ for (const key of keys) {
9120
+ if (value === null || value === void 0 || typeof value !== "object") {
9121
+ return void 0;
9122
+ }
9123
+ value = value[key];
9124
+ }
9125
+ return value;
9126
+ }
9027
9127
  async insert({ id, ...attributes }) {
9028
9128
  if (this.options.timestamps) {
9029
9129
  attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9505,7 +9605,19 @@ class Resource extends EventEmitter {
9505
9605
  */
9506
9606
  getDefinitionHash() {
9507
9607
  const exportedSchema = this.schema.export();
9508
- const stableString = jsonStableStringify(exportedSchema);
9608
+ const stableSchema = {
9609
+ ...exportedSchema,
9610
+ attributes: { ...exportedSchema.attributes }
9611
+ };
9612
+ if (this.options.timestamps) {
9613
+ delete stableSchema.attributes.createdAt;
9614
+ delete stableSchema.attributes.updatedAt;
9615
+ if (stableSchema.options && stableSchema.options.partitions) {
9616
+ delete stableSchema.options.partitions.byCreatedDate;
9617
+ delete stableSchema.options.partitions.byUpdatedDate;
9618
+ }
9619
+ }
9620
+ const stableString = jsonStableStringify(stableSchema);
9509
9621
  return `sha256:${createHash("sha256").update(stableString).digest("hex")}`;
9510
9622
  }
9511
9623
  /**
@@ -9699,7 +9811,7 @@ class Database extends EventEmitter {
9699
9811
  this.version = "1";
9700
9812
  this.s3dbVersion = (() => {
9701
9813
  try {
9702
- return true ? "4.0.1" : "latest";
9814
+ return true ? "4.1.0" : "latest";
9703
9815
  } catch (e) {
9704
9816
  return "latest";
9705
9817
  }