s3db.js 4.1.8 → 4.1.11
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/README.md +786 -1706
- package/dist/s3db.cjs.js +549 -125
- package/dist/s3db.cjs.min.js +14 -12
- package/dist/s3db.es.js +537 -126
- package/dist/s3db.es.min.js +12 -10
- package/dist/s3db.iife.js +549 -125
- package/dist/s3db.iife.min.js +14 -12
- package/package.json +1 -1
package/dist/s3db.cjs.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* istanbul ignore file */
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
5
|
+
|
|
4
6
|
var nanoid = require('nanoid');
|
|
5
7
|
var lodashEs = require('lodash-es');
|
|
6
8
|
var promisePool = require('@supercharge/promise-pool');
|
|
@@ -244,6 +246,8 @@ var substr = 'ab'.substr(-1) === 'b' ?
|
|
|
244
246
|
;
|
|
245
247
|
|
|
246
248
|
const idGenerator = nanoid.customAlphabet(nanoid.urlAlphabet, 22);
|
|
249
|
+
const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
|
|
250
|
+
nanoid.customAlphabet(passwordAlphabet, 12);
|
|
247
251
|
|
|
248
252
|
var domain;
|
|
249
253
|
|
|
@@ -744,42 +748,85 @@ ${JSON.stringify(rest, null, 2)}`;
|
|
|
744
748
|
return `${this.name} | ${this.message}`;
|
|
745
749
|
}
|
|
746
750
|
}
|
|
747
|
-
class
|
|
751
|
+
class S3DBError extends BaseError {
|
|
752
|
+
constructor(message, details = {}) {
|
|
753
|
+
super({ message, ...details });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
class DatabaseError extends S3DBError {
|
|
757
|
+
constructor(message, details = {}) {
|
|
758
|
+
super(message, details);
|
|
759
|
+
Object.assign(this, details);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
class ValidationError extends S3DBError {
|
|
763
|
+
constructor(message, details = {}) {
|
|
764
|
+
super(message, details);
|
|
765
|
+
Object.assign(this, details);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
class AuthenticationError extends S3DBError {
|
|
769
|
+
constructor(message, details = {}) {
|
|
770
|
+
super(message, details);
|
|
771
|
+
Object.assign(this, details);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
class PermissionError extends S3DBError {
|
|
775
|
+
constructor(message, details = {}) {
|
|
776
|
+
super(message, details);
|
|
777
|
+
Object.assign(this, details);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
class EncryptionError extends S3DBError {
|
|
781
|
+
constructor(message, details = {}) {
|
|
782
|
+
super(message, details);
|
|
783
|
+
Object.assign(this, details);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
class ResourceNotFound extends S3DBError {
|
|
787
|
+
constructor({ bucket, resourceName, id, ...rest }) {
|
|
788
|
+
super(`Resource not found: ${resourceName}/${id} [bucket:${bucket}]`, {
|
|
789
|
+
bucket,
|
|
790
|
+
resourceName,
|
|
791
|
+
id,
|
|
792
|
+
...rest
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
class NoSuchBucket extends S3DBError {
|
|
748
797
|
constructor({ bucket, ...rest }) {
|
|
749
|
-
super(
|
|
798
|
+
super(`Bucket does not exists [bucket:${bucket}]`, { bucket, ...rest });
|
|
750
799
|
}
|
|
751
800
|
}
|
|
752
|
-
class NoSuchKey extends
|
|
801
|
+
class NoSuchKey extends S3DBError {
|
|
753
802
|
constructor({ bucket, key, ...rest }) {
|
|
754
|
-
super(
|
|
755
|
-
this.key = key;
|
|
803
|
+
super(`Key [${key}] does not exists [bucket:${bucket}/${key}]`, { bucket, key, ...rest });
|
|
756
804
|
}
|
|
757
805
|
}
|
|
758
806
|
class NotFound extends NoSuchKey {
|
|
759
807
|
}
|
|
760
|
-
class MissingMetadata extends
|
|
808
|
+
class MissingMetadata extends S3DBError {
|
|
761
809
|
constructor({ bucket, ...rest }) {
|
|
762
|
-
super(
|
|
810
|
+
super(`Missing metadata for bucket [bucket:${bucket}]`, { bucket, ...rest });
|
|
763
811
|
}
|
|
764
812
|
}
|
|
765
|
-
class InvalidResourceItem extends
|
|
813
|
+
class InvalidResourceItem extends S3DBError {
|
|
766
814
|
constructor({
|
|
767
815
|
bucket,
|
|
768
816
|
resourceName,
|
|
769
817
|
attributes,
|
|
770
818
|
validation
|
|
771
819
|
}) {
|
|
772
|
-
super({
|
|
820
|
+
super(`This item is not valid. Resource=${resourceName} [bucket:${bucket}].
|
|
821
|
+
${JSON.stringify(validation, null, 2)}`, {
|
|
773
822
|
bucket,
|
|
774
|
-
|
|
775
|
-
|
|
823
|
+
resourceName,
|
|
824
|
+
attributes,
|
|
825
|
+
validation
|
|
776
826
|
});
|
|
777
|
-
this.resourceName = resourceName;
|
|
778
|
-
this.attributes = attributes;
|
|
779
|
-
this.validation = validation;
|
|
780
827
|
}
|
|
781
828
|
}
|
|
782
|
-
class UnknownError extends
|
|
829
|
+
class UnknownError extends S3DBError {
|
|
783
830
|
}
|
|
784
831
|
const ErrorMap = {
|
|
785
832
|
"NotFound": NotFound,
|
|
@@ -808,9 +855,9 @@ class ConnectionString {
|
|
|
808
855
|
}
|
|
809
856
|
}
|
|
810
857
|
defineS3(uri) {
|
|
811
|
-
this.bucket = uri.hostname;
|
|
812
|
-
this.accessKeyId = uri.username;
|
|
813
|
-
this.secretAccessKey = uri.password;
|
|
858
|
+
this.bucket = decodeURIComponent(uri.hostname);
|
|
859
|
+
this.accessKeyId = decodeURIComponent(uri.username);
|
|
860
|
+
this.secretAccessKey = decodeURIComponent(uri.password);
|
|
814
861
|
this.endpoint = S3_DEFAULT_ENDPOINT;
|
|
815
862
|
if (["/", "", null].includes(uri.pathname)) {
|
|
816
863
|
this.keyPrefix = "";
|
|
@@ -822,14 +869,14 @@ class ConnectionString {
|
|
|
822
869
|
defineMinio(uri) {
|
|
823
870
|
this.forcePathStyle = true;
|
|
824
871
|
this.endpoint = uri.origin;
|
|
825
|
-
this.accessKeyId = uri.username;
|
|
826
|
-
this.secretAccessKey = uri.password;
|
|
872
|
+
this.accessKeyId = decodeURIComponent(uri.username);
|
|
873
|
+
this.secretAccessKey = decodeURIComponent(uri.password);
|
|
827
874
|
if (["/", "", null].includes(uri.pathname)) {
|
|
828
875
|
this.bucket = "s3db";
|
|
829
876
|
this.keyPrefix = "";
|
|
830
877
|
} else {
|
|
831
878
|
let [, bucket, ...subpath] = uri.pathname.split("/");
|
|
832
|
-
this.bucket = bucket;
|
|
879
|
+
this.bucket = decodeURIComponent(bucket);
|
|
833
880
|
this.keyPrefix = [...subpath || []].join("/");
|
|
834
881
|
}
|
|
835
882
|
}
|
|
@@ -8838,18 +8885,83 @@ function getBehavior(behaviorName) {
|
|
|
8838
8885
|
const DEFAULT_BEHAVIOR = "user-management";
|
|
8839
8886
|
|
|
8840
8887
|
class Resource extends EventEmitter {
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8888
|
+
/**
|
|
8889
|
+
* Create a new Resource instance
|
|
8890
|
+
* @param {Object} config - Resource configuration
|
|
8891
|
+
* @param {string} config.name - Resource name
|
|
8892
|
+
* @param {Object} config.client - S3 client instance
|
|
8893
|
+
* @param {string} [config.version='v0'] - Resource version
|
|
8894
|
+
* @param {Object} [config.attributes={}] - Resource attributes schema
|
|
8895
|
+
* @param {string} [config.behavior='user-management'] - Resource behavior strategy
|
|
8896
|
+
* @param {string} [config.passphrase='secret'] - Encryption passphrase
|
|
8897
|
+
* @param {number} [config.parallelism=10] - Parallelism for bulk operations
|
|
8898
|
+
* @param {Array} [config.observers=[]] - Observer instances
|
|
8899
|
+
* @param {boolean} [config.cache=false] - Enable caching
|
|
8900
|
+
* @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
|
|
8901
|
+
* @param {boolean} [config.timestamps=false] - Enable automatic timestamps
|
|
8902
|
+
* @param {Object} [config.partitions={}] - Partition definitions
|
|
8903
|
+
* @param {boolean} [config.paranoid=true] - Security flag for dangerous operations
|
|
8904
|
+
* @param {boolean} [config.allNestedObjectsOptional=false] - Make nested objects optional
|
|
8905
|
+
* @param {Object} [config.hooks={}] - Custom hooks
|
|
8906
|
+
* @param {Object} [config.options={}] - Additional options
|
|
8907
|
+
* @example
|
|
8908
|
+
* const users = new Resource({
|
|
8909
|
+
* name: 'users',
|
|
8910
|
+
* client: s3Client,
|
|
8911
|
+
* attributes: {
|
|
8912
|
+
* name: 'string|required',
|
|
8913
|
+
* email: 'string|required',
|
|
8914
|
+
* password: 'secret|required'
|
|
8915
|
+
* },
|
|
8916
|
+
* behavior: 'user-management',
|
|
8917
|
+
* passphrase: 'my-secret-key',
|
|
8918
|
+
* timestamps: true,
|
|
8919
|
+
* partitions: {
|
|
8920
|
+
* byRegion: {
|
|
8921
|
+
* fields: { region: 'string' }
|
|
8922
|
+
* }
|
|
8923
|
+
* },
|
|
8924
|
+
* hooks: {
|
|
8925
|
+
* preInsert: [async (data) => {
|
|
8926
|
+
* console.log('Pre-insert hook:', data);
|
|
8927
|
+
* return data;
|
|
8928
|
+
* }]
|
|
8929
|
+
* }
|
|
8930
|
+
* });
|
|
8931
|
+
*/
|
|
8932
|
+
constructor(config) {
|
|
8852
8933
|
super();
|
|
8934
|
+
const validation = validateResourceConfig(config);
|
|
8935
|
+
if (!validation.isValid) {
|
|
8936
|
+
throw new Error(`Invalid Resource configuration:
|
|
8937
|
+
${validation.errors.join("\n")}`);
|
|
8938
|
+
}
|
|
8939
|
+
const {
|
|
8940
|
+
name,
|
|
8941
|
+
client,
|
|
8942
|
+
version = "1",
|
|
8943
|
+
attributes = {},
|
|
8944
|
+
behavior = DEFAULT_BEHAVIOR,
|
|
8945
|
+
passphrase = "secret",
|
|
8946
|
+
parallelism = 10,
|
|
8947
|
+
observers = [],
|
|
8948
|
+
cache = false,
|
|
8949
|
+
autoDecrypt = true,
|
|
8950
|
+
timestamps = false,
|
|
8951
|
+
partitions = {},
|
|
8952
|
+
paranoid = true,
|
|
8953
|
+
allNestedObjectsOptional = true,
|
|
8954
|
+
hooks = {},
|
|
8955
|
+
options = {}
|
|
8956
|
+
} = config;
|
|
8957
|
+
const mergedOptions = {
|
|
8958
|
+
cache: typeof options.cache === "boolean" ? options.cache : cache,
|
|
8959
|
+
autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
|
|
8960
|
+
timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
|
|
8961
|
+
paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
|
|
8962
|
+
allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
|
|
8963
|
+
partitions: options.partitions || partitions || {}
|
|
8964
|
+
};
|
|
8853
8965
|
this.name = name;
|
|
8854
8966
|
this.client = client;
|
|
8855
8967
|
this.version = version;
|
|
@@ -8857,15 +8969,14 @@ class Resource extends EventEmitter {
|
|
|
8857
8969
|
this.observers = observers;
|
|
8858
8970
|
this.parallelism = parallelism;
|
|
8859
8971
|
this.passphrase = passphrase ?? "secret";
|
|
8860
|
-
this.
|
|
8861
|
-
cache:
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
allNestedObjectsOptional:
|
|
8868
|
-
...options
|
|
8972
|
+
this.config = {
|
|
8973
|
+
cache: mergedOptions.cache,
|
|
8974
|
+
hooks,
|
|
8975
|
+
paranoid: mergedOptions.paranoid,
|
|
8976
|
+
timestamps: mergedOptions.timestamps,
|
|
8977
|
+
partitions: mergedOptions.partitions,
|
|
8978
|
+
autoDecrypt: mergedOptions.autoDecrypt,
|
|
8979
|
+
allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
|
|
8869
8980
|
};
|
|
8870
8981
|
this.hooks = {
|
|
8871
8982
|
preInsert: [],
|
|
@@ -8876,18 +8987,21 @@ class Resource extends EventEmitter {
|
|
|
8876
8987
|
afterDelete: []
|
|
8877
8988
|
};
|
|
8878
8989
|
this.attributes = attributes || {};
|
|
8879
|
-
if (
|
|
8990
|
+
if (this.config.timestamps) {
|
|
8880
8991
|
this.attributes.createdAt = "string|optional";
|
|
8881
8992
|
this.attributes.updatedAt = "string|optional";
|
|
8882
|
-
if (!this.
|
|
8883
|
-
this.
|
|
8993
|
+
if (!this.config.partitions) {
|
|
8994
|
+
this.config.partitions = {};
|
|
8995
|
+
}
|
|
8996
|
+
if (!this.config.partitions.byCreatedDate) {
|
|
8997
|
+
this.config.partitions.byCreatedDate = {
|
|
8884
8998
|
fields: {
|
|
8885
8999
|
createdAt: "date|maxlength:10"
|
|
8886
9000
|
}
|
|
8887
9001
|
};
|
|
8888
9002
|
}
|
|
8889
|
-
if (!this.
|
|
8890
|
-
this.
|
|
9003
|
+
if (!this.config.partitions.byUpdatedDate) {
|
|
9004
|
+
this.config.partitions.byUpdatedDate = {
|
|
8891
9005
|
fields: {
|
|
8892
9006
|
updatedAt: "date|maxlength:10"
|
|
8893
9007
|
}
|
|
@@ -8900,25 +9014,47 @@ class Resource extends EventEmitter {
|
|
|
8900
9014
|
passphrase,
|
|
8901
9015
|
version: this.version,
|
|
8902
9016
|
options: {
|
|
8903
|
-
|
|
8904
|
-
allNestedObjectsOptional: this.
|
|
9017
|
+
autoDecrypt: this.config.autoDecrypt,
|
|
9018
|
+
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
8905
9019
|
}
|
|
8906
9020
|
});
|
|
8907
|
-
this.validatePartitions();
|
|
8908
9021
|
this.setupPartitionHooks();
|
|
8909
|
-
|
|
8910
|
-
|
|
9022
|
+
this.validatePartitions();
|
|
9023
|
+
if (hooks) {
|
|
9024
|
+
for (const [event, hooksArr] of Object.entries(hooks)) {
|
|
8911
9025
|
if (Array.isArray(hooksArr) && this.hooks[event]) {
|
|
8912
9026
|
for (const fn of hooksArr) {
|
|
8913
|
-
|
|
9027
|
+
if (typeof fn === "function") {
|
|
9028
|
+
this.hooks[event].push(fn.bind(this));
|
|
9029
|
+
}
|
|
8914
9030
|
}
|
|
8915
9031
|
}
|
|
8916
9032
|
}
|
|
8917
9033
|
}
|
|
8918
9034
|
}
|
|
9035
|
+
/**
|
|
9036
|
+
* Get resource options (for backward compatibility with tests)
|
|
9037
|
+
*/
|
|
9038
|
+
get options() {
|
|
9039
|
+
return {
|
|
9040
|
+
timestamps: this.config.timestamps,
|
|
9041
|
+
partitions: this.config.partitions || {},
|
|
9042
|
+
cache: this.config.cache,
|
|
9043
|
+
autoDecrypt: this.config.autoDecrypt,
|
|
9044
|
+
paranoid: this.config.paranoid,
|
|
9045
|
+
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
9046
|
+
};
|
|
9047
|
+
}
|
|
8919
9048
|
export() {
|
|
8920
9049
|
const exported = this.schema.export();
|
|
8921
9050
|
exported.behavior = this.behavior;
|
|
9051
|
+
exported.timestamps = this.config.timestamps;
|
|
9052
|
+
exported.partitions = this.config.partitions || {};
|
|
9053
|
+
exported.paranoid = this.config.paranoid;
|
|
9054
|
+
exported.allNestedObjectsOptional = this.config.allNestedObjectsOptional;
|
|
9055
|
+
exported.autoDecrypt = this.config.autoDecrypt;
|
|
9056
|
+
exported.cache = this.config.cache;
|
|
9057
|
+
exported.hooks = this.hooks;
|
|
8922
9058
|
return exported;
|
|
8923
9059
|
}
|
|
8924
9060
|
/**
|
|
@@ -8928,18 +9064,21 @@ class Resource extends EventEmitter {
|
|
|
8928
9064
|
updateAttributes(newAttributes) {
|
|
8929
9065
|
const oldAttributes = this.attributes;
|
|
8930
9066
|
this.attributes = newAttributes;
|
|
8931
|
-
if (this.
|
|
9067
|
+
if (this.config.timestamps) {
|
|
8932
9068
|
newAttributes.createdAt = "string|optional";
|
|
8933
9069
|
newAttributes.updatedAt = "string|optional";
|
|
8934
|
-
if (!this.
|
|
8935
|
-
this.
|
|
9070
|
+
if (!this.config.partitions) {
|
|
9071
|
+
this.config.partitions = {};
|
|
9072
|
+
}
|
|
9073
|
+
if (!this.config.partitions.byCreatedDate) {
|
|
9074
|
+
this.config.partitions.byCreatedDate = {
|
|
8936
9075
|
fields: {
|
|
8937
9076
|
createdAt: "date|maxlength:10"
|
|
8938
9077
|
}
|
|
8939
9078
|
};
|
|
8940
9079
|
}
|
|
8941
|
-
if (!this.
|
|
8942
|
-
this.
|
|
9080
|
+
if (!this.config.partitions.byUpdatedDate) {
|
|
9081
|
+
this.config.partitions.byUpdatedDate = {
|
|
8943
9082
|
fields: {
|
|
8944
9083
|
updatedAt: "date|maxlength:10"
|
|
8945
9084
|
}
|
|
@@ -8951,10 +9090,13 @@ class Resource extends EventEmitter {
|
|
|
8951
9090
|
attributes: newAttributes,
|
|
8952
9091
|
passphrase: this.passphrase,
|
|
8953
9092
|
version: this.version,
|
|
8954
|
-
options:
|
|
9093
|
+
options: {
|
|
9094
|
+
autoDecrypt: this.config.autoDecrypt,
|
|
9095
|
+
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
9096
|
+
}
|
|
8955
9097
|
});
|
|
8956
|
-
this.validatePartitions();
|
|
8957
9098
|
this.setupPartitionHooks();
|
|
9099
|
+
this.validatePartitions();
|
|
8958
9100
|
return { oldAttributes, newAttributes };
|
|
8959
9101
|
}
|
|
8960
9102
|
/**
|
|
@@ -8985,15 +9127,24 @@ class Resource extends EventEmitter {
|
|
|
8985
9127
|
* Setup automatic partition hooks
|
|
8986
9128
|
*/
|
|
8987
9129
|
setupPartitionHooks() {
|
|
8988
|
-
|
|
8989
|
-
|
|
9130
|
+
if (!this.config.partitions) {
|
|
9131
|
+
return;
|
|
9132
|
+
}
|
|
9133
|
+
const partitions = this.config.partitions;
|
|
9134
|
+
if (Object.keys(partitions).length === 0) {
|
|
8990
9135
|
return;
|
|
8991
9136
|
}
|
|
8992
|
-
this.
|
|
9137
|
+
if (!this.hooks.afterInsert) {
|
|
9138
|
+
this.hooks.afterInsert = [];
|
|
9139
|
+
}
|
|
9140
|
+
this.hooks.afterInsert.push(async (data) => {
|
|
8993
9141
|
await this.createPartitionReferences(data);
|
|
8994
9142
|
return data;
|
|
8995
9143
|
});
|
|
8996
|
-
this.
|
|
9144
|
+
if (!this.hooks.afterDelete) {
|
|
9145
|
+
this.hooks.afterDelete = [];
|
|
9146
|
+
}
|
|
9147
|
+
this.hooks.afterDelete.push(async (data) => {
|
|
8997
9148
|
await this.deletePartitionReferences(data);
|
|
8998
9149
|
return data;
|
|
8999
9150
|
});
|
|
@@ -9018,8 +9169,11 @@ class Resource extends EventEmitter {
|
|
|
9018
9169
|
* @throws {Error} If partition fields don't exist in current schema
|
|
9019
9170
|
*/
|
|
9020
9171
|
validatePartitions() {
|
|
9021
|
-
|
|
9022
|
-
|
|
9172
|
+
if (!this.config.partitions) {
|
|
9173
|
+
return;
|
|
9174
|
+
}
|
|
9175
|
+
const partitions = this.config.partitions;
|
|
9176
|
+
if (Object.keys(partitions).length === 0) {
|
|
9023
9177
|
return;
|
|
9024
9178
|
}
|
|
9025
9179
|
const currentAttributes = Object.keys(this.attributes || {});
|
|
@@ -9126,10 +9280,10 @@ class Resource extends EventEmitter {
|
|
|
9126
9280
|
* // Returns: null
|
|
9127
9281
|
*/
|
|
9128
9282
|
getPartitionKey({ partitionName, id, data }) {
|
|
9129
|
-
|
|
9130
|
-
if (!partition) {
|
|
9283
|
+
if (!this.config.partitions || !this.config.partitions[partitionName]) {
|
|
9131
9284
|
throw new Error(`Partition '${partitionName}' not found`);
|
|
9132
9285
|
}
|
|
9286
|
+
const partition = this.config.partitions[partitionName];
|
|
9133
9287
|
const partitionSegments = [];
|
|
9134
9288
|
const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
9135
9289
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -9185,6 +9339,12 @@ class Resource extends EventEmitter {
|
|
|
9185
9339
|
* name: 'Jane Smith',
|
|
9186
9340
|
* email: 'jane@example.com'
|
|
9187
9341
|
* });
|
|
9342
|
+
*
|
|
9343
|
+
* // Insert with auto-generated password for secret field
|
|
9344
|
+
* const user = await resource.insert({
|
|
9345
|
+
* name: 'John Doe',
|
|
9346
|
+
* email: 'john@example.com',
|
|
9347
|
+
* });
|
|
9188
9348
|
*/
|
|
9189
9349
|
async insert({ id, ...attributes }) {
|
|
9190
9350
|
if (this.options.timestamps) {
|
|
@@ -9269,6 +9429,58 @@ class Resource extends EventEmitter {
|
|
|
9269
9429
|
this.emit("get", data);
|
|
9270
9430
|
return data;
|
|
9271
9431
|
} catch (error) {
|
|
9432
|
+
if (error.message.includes("Cipher job failed") || error.message.includes("OperationError") || error.originalError?.message?.includes("Cipher job failed")) {
|
|
9433
|
+
try {
|
|
9434
|
+
console.warn(`Decryption failed for resource ${id}, attempting to get raw metadata`);
|
|
9435
|
+
const request = await this.client.headObject(key);
|
|
9436
|
+
const objectVersion = this.extractVersionFromKey(key) || this.version;
|
|
9437
|
+
const tempSchema = new Schema({
|
|
9438
|
+
name: this.name,
|
|
9439
|
+
attributes: this.attributes,
|
|
9440
|
+
passphrase: this.passphrase,
|
|
9441
|
+
version: objectVersion,
|
|
9442
|
+
options: {
|
|
9443
|
+
...this.config,
|
|
9444
|
+
autoDecrypt: false,
|
|
9445
|
+
// Disable decryption
|
|
9446
|
+
autoEncrypt: false
|
|
9447
|
+
// Disable encryption
|
|
9448
|
+
}
|
|
9449
|
+
});
|
|
9450
|
+
let metadata = await tempSchema.unmapper(request.Metadata);
|
|
9451
|
+
const behaviorImpl = getBehavior(this.behavior);
|
|
9452
|
+
let body = "";
|
|
9453
|
+
if (request.ContentLength > 0) {
|
|
9454
|
+
try {
|
|
9455
|
+
const fullObject = await this.client.getObject(key);
|
|
9456
|
+
body = await streamToString(fullObject.Body);
|
|
9457
|
+
} catch (bodyError) {
|
|
9458
|
+
console.warn(`Failed to read body for resource ${id}:`, bodyError.message);
|
|
9459
|
+
body = "";
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
const { metadata: processedMetadata } = await behaviorImpl.handleGet({
|
|
9463
|
+
resource: this,
|
|
9464
|
+
metadata,
|
|
9465
|
+
body
|
|
9466
|
+
});
|
|
9467
|
+
let data = processedMetadata;
|
|
9468
|
+
data.id = id;
|
|
9469
|
+
data._contentLength = request.ContentLength;
|
|
9470
|
+
data._lastModified = request.LastModified;
|
|
9471
|
+
data._hasContent = request.ContentLength > 0;
|
|
9472
|
+
data._mimeType = request.ContentType || null;
|
|
9473
|
+
data._version = objectVersion;
|
|
9474
|
+
data._decryptionFailed = true;
|
|
9475
|
+
if (request.VersionId) data._versionId = request.VersionId;
|
|
9476
|
+
if (request.Expiration) data._expiresAt = request.Expiration;
|
|
9477
|
+
data._definitionHash = this.getDefinitionHash();
|
|
9478
|
+
this.emit("get", data);
|
|
9479
|
+
return data;
|
|
9480
|
+
} catch (fallbackError) {
|
|
9481
|
+
console.error(`Fallback attempt also failed for resource ${id}:`, fallbackError.message);
|
|
9482
|
+
}
|
|
9483
|
+
}
|
|
9272
9484
|
const enhancedError = new Error(`Failed to get resource with id '${id}': ${error.message}`);
|
|
9273
9485
|
enhancedError.originalError = error;
|
|
9274
9486
|
enhancedError.resourceId = id;
|
|
@@ -9315,7 +9527,7 @@ class Resource extends EventEmitter {
|
|
|
9315
9527
|
*/
|
|
9316
9528
|
async update(id, attributes) {
|
|
9317
9529
|
const live = await this.get(id);
|
|
9318
|
-
if (this.
|
|
9530
|
+
if (this.config.timestamps) {
|
|
9319
9531
|
attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9320
9532
|
}
|
|
9321
9533
|
const preProcessedData = await this.executeHooks("preUpdate", attributes);
|
|
@@ -9437,7 +9649,7 @@ class Resource extends EventEmitter {
|
|
|
9437
9649
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
9438
9650
|
let prefix;
|
|
9439
9651
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
9440
|
-
const partitionDef = this.
|
|
9652
|
+
const partitionDef = this.config.partitions[partition];
|
|
9441
9653
|
if (!partitionDef) {
|
|
9442
9654
|
throw new Error(`Partition '${partition}' not found`);
|
|
9443
9655
|
}
|
|
@@ -9522,9 +9734,9 @@ class Resource extends EventEmitter {
|
|
|
9522
9734
|
return results;
|
|
9523
9735
|
}
|
|
9524
9736
|
async deleteAll() {
|
|
9525
|
-
if (this.
|
|
9737
|
+
if (this.config.paranoid !== false) {
|
|
9526
9738
|
throw new Error(
|
|
9527
|
-
`deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.
|
|
9739
|
+
`deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
|
|
9528
9740
|
);
|
|
9529
9741
|
}
|
|
9530
9742
|
const prefix = `resource=${this.name}/v=${this.version}`;
|
|
@@ -9541,9 +9753,9 @@ class Resource extends EventEmitter {
|
|
|
9541
9753
|
* @returns {Promise<Object>} Deletion report
|
|
9542
9754
|
*/
|
|
9543
9755
|
async deleteAllData() {
|
|
9544
|
-
if (this.
|
|
9756
|
+
if (this.config.paranoid !== false) {
|
|
9545
9757
|
throw new Error(
|
|
9546
|
-
`deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.
|
|
9758
|
+
`deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
|
|
9547
9759
|
);
|
|
9548
9760
|
}
|
|
9549
9761
|
const prefix = `resource=${this.name}`;
|
|
@@ -9586,10 +9798,10 @@ class Resource extends EventEmitter {
|
|
|
9586
9798
|
async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
9587
9799
|
let prefix;
|
|
9588
9800
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
9589
|
-
|
|
9590
|
-
if (!partitionDef) {
|
|
9801
|
+
if (!this.config.partitions || !this.config.partitions[partition]) {
|
|
9591
9802
|
throw new Error(`Partition '${partition}' not found`);
|
|
9592
9803
|
}
|
|
9804
|
+
const partitionDef = this.config.partitions[partition];
|
|
9593
9805
|
const partitionSegments = [];
|
|
9594
9806
|
const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
9595
9807
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -9658,16 +9870,32 @@ class Resource extends EventEmitter {
|
|
|
9658
9870
|
if (limit) {
|
|
9659
9871
|
filteredIds2 = filteredIds2.slice(0, limit);
|
|
9660
9872
|
}
|
|
9661
|
-
const { results: results2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).
|
|
9662
|
-
|
|
9873
|
+
const { results: results2, errors: errors2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
|
|
9874
|
+
console.warn(`Failed to get resource ${id}:`, error.message);
|
|
9875
|
+
return null;
|
|
9876
|
+
}).process(async (id) => {
|
|
9877
|
+
try {
|
|
9878
|
+
return await this.get(id);
|
|
9879
|
+
} catch (error) {
|
|
9880
|
+
if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
|
|
9881
|
+
console.warn(`Decryption failed for ${id}, returning basic info`);
|
|
9882
|
+
return {
|
|
9883
|
+
id,
|
|
9884
|
+
_decryptionFailed: true,
|
|
9885
|
+
_error: error.message
|
|
9886
|
+
};
|
|
9887
|
+
}
|
|
9888
|
+
throw error;
|
|
9889
|
+
}
|
|
9663
9890
|
});
|
|
9664
|
-
|
|
9665
|
-
|
|
9891
|
+
const validResults2 = results2.filter((item) => item !== null);
|
|
9892
|
+
this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
|
|
9893
|
+
return validResults2;
|
|
9666
9894
|
}
|
|
9667
|
-
|
|
9668
|
-
if (!partitionDef) {
|
|
9895
|
+
if (!this.config.partitions || !this.config.partitions[partition]) {
|
|
9669
9896
|
throw new Error(`Partition '${partition}' not found`);
|
|
9670
9897
|
}
|
|
9898
|
+
const partitionDef = this.config.partitions[partition];
|
|
9671
9899
|
const partitionSegments = [];
|
|
9672
9900
|
const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
9673
9901
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -9693,11 +9921,29 @@ class Resource extends EventEmitter {
|
|
|
9693
9921
|
if (limit) {
|
|
9694
9922
|
filteredIds = filteredIds.slice(0, limit);
|
|
9695
9923
|
}
|
|
9696
|
-
const { results } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).
|
|
9697
|
-
|
|
9924
|
+
const { results, errors } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
|
|
9925
|
+
console.warn(`Failed to get partition resource ${id}:`, error.message);
|
|
9926
|
+
return null;
|
|
9927
|
+
}).process(async (id) => {
|
|
9928
|
+
try {
|
|
9929
|
+
return await this.getFromPartition({ id, partitionName: partition, partitionValues });
|
|
9930
|
+
} catch (error) {
|
|
9931
|
+
if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
|
|
9932
|
+
console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
|
|
9933
|
+
return {
|
|
9934
|
+
id,
|
|
9935
|
+
_partition: partition,
|
|
9936
|
+
_partitionValues: partitionValues,
|
|
9937
|
+
_decryptionFailed: true,
|
|
9938
|
+
_error: error.message
|
|
9939
|
+
};
|
|
9940
|
+
}
|
|
9941
|
+
throw error;
|
|
9942
|
+
}
|
|
9698
9943
|
});
|
|
9699
|
-
|
|
9700
|
-
|
|
9944
|
+
const validResults = results.filter((item) => item !== null);
|
|
9945
|
+
this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
|
|
9946
|
+
return validResults;
|
|
9701
9947
|
}
|
|
9702
9948
|
/**
|
|
9703
9949
|
* Get multiple resources by their IDs
|
|
@@ -9708,11 +9954,30 @@ class Resource extends EventEmitter {
|
|
|
9708
9954
|
* users.forEach(user => console.log(user.name));
|
|
9709
9955
|
*/
|
|
9710
9956
|
async getMany(ids) {
|
|
9711
|
-
const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).
|
|
9957
|
+
const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
|
|
9958
|
+
console.warn(`Failed to get resource ${id}:`, error.message);
|
|
9959
|
+
return {
|
|
9960
|
+
id,
|
|
9961
|
+
_error: error.message,
|
|
9962
|
+
_decryptionFailed: error.message.includes("Cipher job failed") || error.message.includes("OperationError")
|
|
9963
|
+
};
|
|
9964
|
+
}).process(async (id) => {
|
|
9712
9965
|
this.emit("id", id);
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9966
|
+
try {
|
|
9967
|
+
const data = await this.get(id);
|
|
9968
|
+
this.emit("data", data);
|
|
9969
|
+
return data;
|
|
9970
|
+
} catch (error) {
|
|
9971
|
+
if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
|
|
9972
|
+
console.warn(`Decryption failed for ${id}, returning basic info`);
|
|
9973
|
+
return {
|
|
9974
|
+
id,
|
|
9975
|
+
_decryptionFailed: true,
|
|
9976
|
+
_error: error.message
|
|
9977
|
+
};
|
|
9978
|
+
}
|
|
9979
|
+
throw error;
|
|
9980
|
+
}
|
|
9716
9981
|
});
|
|
9717
9982
|
this.emit("getMany", ids.length);
|
|
9718
9983
|
return results;
|
|
@@ -9727,9 +9992,28 @@ class Resource extends EventEmitter {
|
|
|
9727
9992
|
async getAll() {
|
|
9728
9993
|
let ids = await this.listIds();
|
|
9729
9994
|
if (ids.length === 0) return [];
|
|
9730
|
-
const { results } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).
|
|
9731
|
-
|
|
9732
|
-
return
|
|
9995
|
+
const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
|
|
9996
|
+
console.warn(`Failed to get resource ${id}:`, error.message);
|
|
9997
|
+
return {
|
|
9998
|
+
id,
|
|
9999
|
+
_error: error.message,
|
|
10000
|
+
_decryptionFailed: error.message.includes("Cipher job failed") || error.message.includes("OperationError")
|
|
10001
|
+
};
|
|
10002
|
+
}).process(async (id) => {
|
|
10003
|
+
try {
|
|
10004
|
+
const data = await this.get(id);
|
|
10005
|
+
return data;
|
|
10006
|
+
} catch (error) {
|
|
10007
|
+
if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
|
|
10008
|
+
console.warn(`Decryption failed for ${id}, returning basic info`);
|
|
10009
|
+
return {
|
|
10010
|
+
id,
|
|
10011
|
+
_decryptionFailed: true,
|
|
10012
|
+
_error: error.message
|
|
10013
|
+
};
|
|
10014
|
+
}
|
|
10015
|
+
throw error;
|
|
10016
|
+
}
|
|
9733
10017
|
});
|
|
9734
10018
|
this.emit("getAll", results.length);
|
|
9735
10019
|
return results;
|
|
@@ -9916,16 +10200,14 @@ class Resource extends EventEmitter {
|
|
|
9916
10200
|
}
|
|
9917
10201
|
/**
|
|
9918
10202
|
* Generate definition hash for this resource
|
|
9919
|
-
* @returns {string} SHA256 hash of the
|
|
10203
|
+
* @returns {string} SHA256 hash of the resource definition (name + attributes)
|
|
9920
10204
|
*/
|
|
9921
10205
|
getDefinitionHash() {
|
|
9922
|
-
const
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
}
|
|
9928
|
-
const stableString = jsonStableStringify(stableAttributes);
|
|
10206
|
+
const definition = {
|
|
10207
|
+
attributes: this.attributes,
|
|
10208
|
+
behavior: this.behavior
|
|
10209
|
+
};
|
|
10210
|
+
const stableString = jsonStableStringify(definition);
|
|
9929
10211
|
return `sha256:${crypto.createHash("sha256").update(stableString).digest("hex")}`;
|
|
9930
10212
|
}
|
|
9931
10213
|
/**
|
|
@@ -9944,14 +10226,34 @@ class Resource extends EventEmitter {
|
|
|
9944
10226
|
* @returns {Object} Schema object for the version
|
|
9945
10227
|
*/
|
|
9946
10228
|
async getSchemaForVersion(version) {
|
|
9947
|
-
|
|
10229
|
+
if (version === this.version) {
|
|
10230
|
+
return this.schema;
|
|
10231
|
+
}
|
|
10232
|
+
try {
|
|
10233
|
+
const compatibleSchema = new Schema({
|
|
10234
|
+
name: this.name,
|
|
10235
|
+
attributes: this.attributes,
|
|
10236
|
+
passphrase: this.passphrase,
|
|
10237
|
+
version,
|
|
10238
|
+
options: {
|
|
10239
|
+
...this.config,
|
|
10240
|
+
// For older versions, be more lenient with decryption
|
|
10241
|
+
autoDecrypt: true,
|
|
10242
|
+
autoEncrypt: true
|
|
10243
|
+
}
|
|
10244
|
+
});
|
|
10245
|
+
return compatibleSchema;
|
|
10246
|
+
} catch (error) {
|
|
10247
|
+
console.warn(`Failed to create compatible schema for version ${version}, using current schema:`, error.message);
|
|
10248
|
+
return this.schema;
|
|
10249
|
+
}
|
|
9948
10250
|
}
|
|
9949
10251
|
/**
|
|
9950
10252
|
* Create partition references after insert
|
|
9951
10253
|
* @param {Object} data - Inserted object data
|
|
9952
10254
|
*/
|
|
9953
10255
|
async createPartitionReferences(data) {
|
|
9954
|
-
const partitions = this.
|
|
10256
|
+
const partitions = this.config.partitions;
|
|
9955
10257
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
9956
10258
|
return;
|
|
9957
10259
|
}
|
|
@@ -9982,7 +10284,7 @@ class Resource extends EventEmitter {
|
|
|
9982
10284
|
* @param {Object} data - Deleted object data
|
|
9983
10285
|
*/
|
|
9984
10286
|
async deletePartitionReferences(data) {
|
|
9985
|
-
const partitions = this.
|
|
10287
|
+
const partitions = this.config.partitions;
|
|
9986
10288
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
9987
10289
|
return;
|
|
9988
10290
|
}
|
|
@@ -10074,7 +10376,7 @@ class Resource extends EventEmitter {
|
|
|
10074
10376
|
* @param {Object} data - Updated object data
|
|
10075
10377
|
*/
|
|
10076
10378
|
async updatePartitionReferences(data) {
|
|
10077
|
-
const partitions = this.
|
|
10379
|
+
const partitions = this.config.partitions;
|
|
10078
10380
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
10079
10381
|
return;
|
|
10080
10382
|
}
|
|
@@ -10130,10 +10432,10 @@ class Resource extends EventEmitter {
|
|
|
10130
10432
|
* });
|
|
10131
10433
|
*/
|
|
10132
10434
|
async getFromPartition({ id, partitionName, partitionValues = {} }) {
|
|
10133
|
-
|
|
10134
|
-
if (!partition) {
|
|
10435
|
+
if (!this.config.partitions || !this.config.partitions[partitionName]) {
|
|
10135
10436
|
throw new Error(`Partition '${partitionName}' not found`);
|
|
10136
10437
|
}
|
|
10438
|
+
const partition = this.config.partitions[partitionName];
|
|
10137
10439
|
const partitionSegments = [];
|
|
10138
10440
|
const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
10139
10441
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -10181,6 +10483,98 @@ class Resource extends EventEmitter {
|
|
|
10181
10483
|
return data;
|
|
10182
10484
|
}
|
|
10183
10485
|
}
|
|
10486
|
+
function validateResourceConfig(config) {
|
|
10487
|
+
const errors = [];
|
|
10488
|
+
if (!config.name) {
|
|
10489
|
+
errors.push("Resource 'name' is required");
|
|
10490
|
+
} else if (typeof config.name !== "string") {
|
|
10491
|
+
errors.push("Resource 'name' must be a string");
|
|
10492
|
+
} else if (config.name.trim() === "") {
|
|
10493
|
+
errors.push("Resource 'name' cannot be empty");
|
|
10494
|
+
}
|
|
10495
|
+
if (!config.client) {
|
|
10496
|
+
errors.push("S3 'client' is required");
|
|
10497
|
+
}
|
|
10498
|
+
if (!config.attributes) {
|
|
10499
|
+
errors.push("Resource 'attributes' are required");
|
|
10500
|
+
} else if (typeof config.attributes !== "object" || Array.isArray(config.attributes)) {
|
|
10501
|
+
errors.push("Resource 'attributes' must be an object");
|
|
10502
|
+
} else if (Object.keys(config.attributes).length === 0) {
|
|
10503
|
+
errors.push("Resource 'attributes' cannot be empty");
|
|
10504
|
+
}
|
|
10505
|
+
if (config.version !== void 0 && typeof config.version !== "string") {
|
|
10506
|
+
errors.push("Resource 'version' must be a string");
|
|
10507
|
+
}
|
|
10508
|
+
if (config.behavior !== void 0 && typeof config.behavior !== "string") {
|
|
10509
|
+
errors.push("Resource 'behavior' must be a string");
|
|
10510
|
+
}
|
|
10511
|
+
if (config.passphrase !== void 0 && typeof config.passphrase !== "string") {
|
|
10512
|
+
errors.push("Resource 'passphrase' must be a string");
|
|
10513
|
+
}
|
|
10514
|
+
if (config.parallelism !== void 0) {
|
|
10515
|
+
if (typeof config.parallelism !== "number" || !Number.isInteger(config.parallelism)) {
|
|
10516
|
+
errors.push("Resource 'parallelism' must be an integer");
|
|
10517
|
+
} else if (config.parallelism < 1) {
|
|
10518
|
+
errors.push("Resource 'parallelism' must be greater than 0");
|
|
10519
|
+
}
|
|
10520
|
+
}
|
|
10521
|
+
if (config.observers !== void 0 && !Array.isArray(config.observers)) {
|
|
10522
|
+
errors.push("Resource 'observers' must be an array");
|
|
10523
|
+
}
|
|
10524
|
+
const booleanFields = ["cache", "autoDecrypt", "timestamps", "paranoid", "allNestedObjectsOptional"];
|
|
10525
|
+
for (const field of booleanFields) {
|
|
10526
|
+
if (config[field] !== void 0 && typeof config[field] !== "boolean") {
|
|
10527
|
+
errors.push(`Resource '${field}' must be a boolean`);
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
if (config.partitions !== void 0) {
|
|
10531
|
+
if (typeof config.partitions !== "object" || Array.isArray(config.partitions)) {
|
|
10532
|
+
errors.push("Resource 'partitions' must be an object");
|
|
10533
|
+
} else {
|
|
10534
|
+
for (const [partitionName, partitionDef] of Object.entries(config.partitions)) {
|
|
10535
|
+
if (typeof partitionDef !== "object" || Array.isArray(partitionDef)) {
|
|
10536
|
+
errors.push(`Partition '${partitionName}' must be an object`);
|
|
10537
|
+
} else if (!partitionDef.fields) {
|
|
10538
|
+
errors.push(`Partition '${partitionName}' must have a 'fields' property`);
|
|
10539
|
+
} else if (typeof partitionDef.fields !== "object" || Array.isArray(partitionDef.fields)) {
|
|
10540
|
+
errors.push(`Partition '${partitionName}.fields' must be an object`);
|
|
10541
|
+
} else {
|
|
10542
|
+
for (const [fieldName, fieldType] of Object.entries(partitionDef.fields)) {
|
|
10543
|
+
if (typeof fieldType !== "string") {
|
|
10544
|
+
errors.push(`Partition '${partitionName}.fields.${fieldName}' must be a string`);
|
|
10545
|
+
}
|
|
10546
|
+
}
|
|
10547
|
+
}
|
|
10548
|
+
}
|
|
10549
|
+
}
|
|
10550
|
+
}
|
|
10551
|
+
if (config.hooks !== void 0) {
|
|
10552
|
+
if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
|
|
10553
|
+
errors.push("Resource 'hooks' must be an object");
|
|
10554
|
+
} else {
|
|
10555
|
+
const validHookEvents = ["preInsert", "afterInsert", "preUpdate", "afterUpdate", "preDelete", "afterDelete"];
|
|
10556
|
+
for (const [event, hooksArr] of Object.entries(config.hooks)) {
|
|
10557
|
+
if (!validHookEvents.includes(event)) {
|
|
10558
|
+
errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
|
|
10559
|
+
} else if (!Array.isArray(hooksArr)) {
|
|
10560
|
+
errors.push(`Resource 'hooks.${event}' must be an array`);
|
|
10561
|
+
} else {
|
|
10562
|
+
for (let i = 0; i < hooksArr.length; i++) {
|
|
10563
|
+
const hook = hooksArr[i];
|
|
10564
|
+
if (typeof hook !== "function") {
|
|
10565
|
+
if (typeof hook === "string") continue;
|
|
10566
|
+
continue;
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
}
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
10572
|
+
}
|
|
10573
|
+
return {
|
|
10574
|
+
isValid: errors.length === 0,
|
|
10575
|
+
errors
|
|
10576
|
+
};
|
|
10577
|
+
}
|
|
10184
10578
|
|
|
10185
10579
|
class Database extends EventEmitter {
|
|
10186
10580
|
constructor(options) {
|
|
@@ -10188,7 +10582,7 @@ class Database extends EventEmitter {
|
|
|
10188
10582
|
this.version = "1";
|
|
10189
10583
|
this.s3dbVersion = (() => {
|
|
10190
10584
|
try {
|
|
10191
|
-
return true ? "4.1.
|
|
10585
|
+
return true ? "4.1.10" : "latest";
|
|
10192
10586
|
} catch (e) {
|
|
10193
10587
|
return "latest";
|
|
10194
10588
|
}
|
|
@@ -10201,10 +10595,21 @@ class Database extends EventEmitter {
|
|
|
10201
10595
|
this.plugins = options.plugins || [];
|
|
10202
10596
|
this.cache = options.cache;
|
|
10203
10597
|
this.passphrase = options.passphrase || "secret";
|
|
10598
|
+
let connectionString = options.connectionString;
|
|
10599
|
+
if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
|
|
10600
|
+
connectionString = ConnectionString.buildFromParams({
|
|
10601
|
+
bucket: options.bucket,
|
|
10602
|
+
region: options.region,
|
|
10603
|
+
accessKeyId: options.accessKeyId,
|
|
10604
|
+
secretAccessKey: options.secretAccessKey,
|
|
10605
|
+
endpoint: options.endpoint,
|
|
10606
|
+
forcePathStyle: options.forcePathStyle
|
|
10607
|
+
});
|
|
10608
|
+
}
|
|
10204
10609
|
this.client = options.client || new Client({
|
|
10205
10610
|
verbose: this.verbose,
|
|
10206
10611
|
parallelism: this.parallelism,
|
|
10207
|
-
connectionString
|
|
10612
|
+
connectionString
|
|
10208
10613
|
});
|
|
10209
10614
|
this.bucket = this.client.bucket;
|
|
10210
10615
|
this.keyPrefix = this.client.keyPrefix;
|
|
@@ -10229,15 +10634,18 @@ class Database extends EventEmitter {
|
|
|
10229
10634
|
name,
|
|
10230
10635
|
client: this.client,
|
|
10231
10636
|
version: currentVersion,
|
|
10232
|
-
options: {
|
|
10233
|
-
...versionData.options,
|
|
10234
|
-
partitions: resourceMetadata.partitions || versionData.options?.partitions || {}
|
|
10235
|
-
},
|
|
10236
10637
|
attributes: versionData.attributes,
|
|
10237
10638
|
behavior: versionData.behavior || "user-management",
|
|
10238
10639
|
parallelism: this.parallelism,
|
|
10239
10640
|
passphrase: this.passphrase,
|
|
10240
|
-
observers: [this]
|
|
10641
|
+
observers: [this],
|
|
10642
|
+
cache: this.cache,
|
|
10643
|
+
timestamps: versionData.options?.timestamps || false,
|
|
10644
|
+
partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
|
|
10645
|
+
paranoid: versionData.options?.paranoid !== false,
|
|
10646
|
+
allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
|
|
10647
|
+
autoDecrypt: versionData.options?.autoDecrypt !== false,
|
|
10648
|
+
hooks: {}
|
|
10241
10649
|
});
|
|
10242
10650
|
}
|
|
10243
10651
|
}
|
|
@@ -10306,7 +10714,7 @@ class Database extends EventEmitter {
|
|
|
10306
10714
|
generateDefinitionHash(definition, behavior = void 0) {
|
|
10307
10715
|
const attributes = definition.attributes;
|
|
10308
10716
|
const stableAttributes = { ...attributes };
|
|
10309
|
-
if (definition.
|
|
10717
|
+
if (definition.timestamps) {
|
|
10310
10718
|
delete stableAttributes.createdAt;
|
|
10311
10719
|
delete stableAttributes.updatedAt;
|
|
10312
10720
|
}
|
|
@@ -10368,14 +10776,21 @@ class Database extends EventEmitter {
|
|
|
10368
10776
|
}
|
|
10369
10777
|
metadata.resources[name] = {
|
|
10370
10778
|
currentVersion: version,
|
|
10371
|
-
partitions:
|
|
10779
|
+
partitions: resource.config.partitions || {},
|
|
10372
10780
|
versions: {
|
|
10373
10781
|
...existingResource?.versions,
|
|
10374
10782
|
// Preserve previous versions
|
|
10375
10783
|
[version]: {
|
|
10376
10784
|
hash: definitionHash,
|
|
10377
10785
|
attributes: resourceDef.attributes,
|
|
10378
|
-
options:
|
|
10786
|
+
options: {
|
|
10787
|
+
timestamps: resource.config.timestamps,
|
|
10788
|
+
partitions: resource.config.partitions,
|
|
10789
|
+
paranoid: resource.config.paranoid,
|
|
10790
|
+
allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
|
|
10791
|
+
autoDecrypt: resource.config.autoDecrypt,
|
|
10792
|
+
cache: resource.config.cache
|
|
10793
|
+
},
|
|
10379
10794
|
behavior: resourceDef.behavior || "user-management",
|
|
10380
10795
|
createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
|
|
10381
10796
|
}
|
|
@@ -10428,10 +10843,9 @@ class Database extends EventEmitter {
|
|
|
10428
10843
|
observers: [],
|
|
10429
10844
|
client: this.client,
|
|
10430
10845
|
version: "temp",
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
}
|
|
10846
|
+
passphrase: this.passphrase,
|
|
10847
|
+
cache: this.cache,
|
|
10848
|
+
...options
|
|
10435
10849
|
});
|
|
10436
10850
|
const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
|
|
10437
10851
|
const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
|
|
@@ -10471,7 +10885,7 @@ class Database extends EventEmitter {
|
|
|
10471
10885
|
async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
|
|
10472
10886
|
if (this.resources[name]) {
|
|
10473
10887
|
const existingResource = this.resources[name];
|
|
10474
|
-
Object.assign(existingResource.
|
|
10888
|
+
Object.assign(existingResource.config, {
|
|
10475
10889
|
cache: this.cache,
|
|
10476
10890
|
...options
|
|
10477
10891
|
});
|
|
@@ -10498,10 +10912,9 @@ class Database extends EventEmitter {
|
|
|
10498
10912
|
observers: [this],
|
|
10499
10913
|
client: this.client,
|
|
10500
10914
|
version,
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
}
|
|
10915
|
+
passphrase: this.passphrase,
|
|
10916
|
+
cache: this.cache,
|
|
10917
|
+
...options
|
|
10505
10918
|
});
|
|
10506
10919
|
this.resources[name] = resource;
|
|
10507
10920
|
await this.uploadMetadataFile();
|
|
@@ -17203,6 +17616,7 @@ class CachePlugin extends Plugin {
|
|
|
17203
17616
|
}
|
|
17204
17617
|
}
|
|
17205
17618
|
|
|
17619
|
+
exports.AuthenticationError = AuthenticationError;
|
|
17206
17620
|
exports.BaseError = BaseError;
|
|
17207
17621
|
exports.Cache = Cache;
|
|
17208
17622
|
exports.CachePlugin = CachePlugin;
|
|
@@ -17210,6 +17624,8 @@ exports.Client = Client;
|
|
|
17210
17624
|
exports.ConnectionString = ConnectionString;
|
|
17211
17625
|
exports.CostsPlugin = CostsPlugin;
|
|
17212
17626
|
exports.Database = Database;
|
|
17627
|
+
exports.DatabaseError = DatabaseError;
|
|
17628
|
+
exports.EncryptionError = EncryptionError;
|
|
17213
17629
|
exports.ErrorMap = ErrorMap;
|
|
17214
17630
|
exports.InvalidResourceItem = InvalidResourceItem;
|
|
17215
17631
|
exports.MemoryCache = MemoryCache;
|
|
@@ -17217,20 +17633,28 @@ exports.MissingMetadata = MissingMetadata;
|
|
|
17217
17633
|
exports.NoSuchBucket = NoSuchBucket;
|
|
17218
17634
|
exports.NoSuchKey = NoSuchKey;
|
|
17219
17635
|
exports.NotFound = NotFound;
|
|
17636
|
+
exports.PermissionError = PermissionError;
|
|
17220
17637
|
exports.Plugin = Plugin;
|
|
17221
17638
|
exports.PluginObject = PluginObject;
|
|
17222
17639
|
exports.ResourceIdsPageReader = ResourceIdsPageReader;
|
|
17223
17640
|
exports.ResourceIdsReader = ResourceIdsReader;
|
|
17641
|
+
exports.ResourceNotFound = ResourceNotFound;
|
|
17642
|
+
exports.ResourceNotFoundError = ResourceNotFound;
|
|
17224
17643
|
exports.ResourceReader = ResourceReader;
|
|
17225
17644
|
exports.ResourceWriter = ResourceWriter;
|
|
17226
17645
|
exports.S3Cache = S3Cache;
|
|
17646
|
+
exports.S3DB = S3db;
|
|
17647
|
+
exports.S3DBError = S3DBError;
|
|
17227
17648
|
exports.S3_DEFAULT_ENDPOINT = S3_DEFAULT_ENDPOINT;
|
|
17228
17649
|
exports.S3_DEFAULT_REGION = S3_DEFAULT_REGION;
|
|
17229
17650
|
exports.S3db = S3db;
|
|
17651
|
+
exports.S3dbError = S3DBError;
|
|
17230
17652
|
exports.UnknownError = UnknownError;
|
|
17653
|
+
exports.ValidationError = ValidationError;
|
|
17231
17654
|
exports.Validator = Validator;
|
|
17232
17655
|
exports.ValidatorManager = ValidatorManager;
|
|
17233
17656
|
exports.decrypt = decrypt;
|
|
17657
|
+
exports.default = S3db;
|
|
17234
17658
|
exports.encrypt = encrypt;
|
|
17235
17659
|
exports.sha256 = sha256;
|
|
17236
17660
|
exports.streamToString = streamToString;
|