s3db.js 4.1.10 → 4.1.13
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 +392 -111
- package/dist/s3db.cjs.min.js +14 -12
- package/dist/s3db.es.js +380 -112
- package/dist/s3db.es.min.js +11 -9
- package/dist/s3db.iife.js +392 -111
- package/dist/s3db.iife.min.js +14 -12
- package/package.json +14 -14
package/dist/s3db.es.js
CHANGED
|
@@ -242,6 +242,8 @@ var substr = 'ab'.substr(-1) === 'b' ?
|
|
|
242
242
|
;
|
|
243
243
|
|
|
244
244
|
const idGenerator = customAlphabet(urlAlphabet, 22);
|
|
245
|
+
const passwordAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
|
|
246
|
+
customAlphabet(passwordAlphabet, 12);
|
|
245
247
|
|
|
246
248
|
var domain;
|
|
247
249
|
|
|
@@ -742,42 +744,85 @@ ${JSON.stringify(rest, null, 2)}`;
|
|
|
742
744
|
return `${this.name} | ${this.message}`;
|
|
743
745
|
}
|
|
744
746
|
}
|
|
745
|
-
class
|
|
747
|
+
class S3DBError extends BaseError {
|
|
748
|
+
constructor(message, details = {}) {
|
|
749
|
+
super({ message, ...details });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
class DatabaseError extends S3DBError {
|
|
753
|
+
constructor(message, details = {}) {
|
|
754
|
+
super(message, details);
|
|
755
|
+
Object.assign(this, details);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
class ValidationError extends S3DBError {
|
|
759
|
+
constructor(message, details = {}) {
|
|
760
|
+
super(message, details);
|
|
761
|
+
Object.assign(this, details);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
class AuthenticationError extends S3DBError {
|
|
765
|
+
constructor(message, details = {}) {
|
|
766
|
+
super(message, details);
|
|
767
|
+
Object.assign(this, details);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
class PermissionError extends S3DBError {
|
|
771
|
+
constructor(message, details = {}) {
|
|
772
|
+
super(message, details);
|
|
773
|
+
Object.assign(this, details);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
class EncryptionError extends S3DBError {
|
|
777
|
+
constructor(message, details = {}) {
|
|
778
|
+
super(message, details);
|
|
779
|
+
Object.assign(this, details);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
class ResourceNotFound extends S3DBError {
|
|
783
|
+
constructor({ bucket, resourceName, id, ...rest }) {
|
|
784
|
+
super(`Resource not found: ${resourceName}/${id} [bucket:${bucket}]`, {
|
|
785
|
+
bucket,
|
|
786
|
+
resourceName,
|
|
787
|
+
id,
|
|
788
|
+
...rest
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
class NoSuchBucket extends S3DBError {
|
|
746
793
|
constructor({ bucket, ...rest }) {
|
|
747
|
-
super(
|
|
794
|
+
super(`Bucket does not exists [bucket:${bucket}]`, { bucket, ...rest });
|
|
748
795
|
}
|
|
749
796
|
}
|
|
750
|
-
class NoSuchKey extends
|
|
797
|
+
class NoSuchKey extends S3DBError {
|
|
751
798
|
constructor({ bucket, key, ...rest }) {
|
|
752
|
-
super(
|
|
753
|
-
this.key = key;
|
|
799
|
+
super(`Key [${key}] does not exists [bucket:${bucket}/${key}]`, { bucket, key, ...rest });
|
|
754
800
|
}
|
|
755
801
|
}
|
|
756
802
|
class NotFound extends NoSuchKey {
|
|
757
803
|
}
|
|
758
|
-
class MissingMetadata extends
|
|
804
|
+
class MissingMetadata extends S3DBError {
|
|
759
805
|
constructor({ bucket, ...rest }) {
|
|
760
|
-
super(
|
|
806
|
+
super(`Missing metadata for bucket [bucket:${bucket}]`, { bucket, ...rest });
|
|
761
807
|
}
|
|
762
808
|
}
|
|
763
|
-
class InvalidResourceItem extends
|
|
809
|
+
class InvalidResourceItem extends S3DBError {
|
|
764
810
|
constructor({
|
|
765
811
|
bucket,
|
|
766
812
|
resourceName,
|
|
767
813
|
attributes,
|
|
768
814
|
validation
|
|
769
815
|
}) {
|
|
770
|
-
super({
|
|
816
|
+
super(`This item is not valid. Resource=${resourceName} [bucket:${bucket}].
|
|
817
|
+
${JSON.stringify(validation, null, 2)}`, {
|
|
771
818
|
bucket,
|
|
772
|
-
|
|
773
|
-
|
|
819
|
+
resourceName,
|
|
820
|
+
attributes,
|
|
821
|
+
validation
|
|
774
822
|
});
|
|
775
|
-
this.resourceName = resourceName;
|
|
776
|
-
this.attributes = attributes;
|
|
777
|
-
this.validation = validation;
|
|
778
823
|
}
|
|
779
824
|
}
|
|
780
|
-
class UnknownError extends
|
|
825
|
+
class UnknownError extends S3DBError {
|
|
781
826
|
}
|
|
782
827
|
const ErrorMap = {
|
|
783
828
|
"NotFound": NotFound,
|
|
@@ -806,9 +851,9 @@ class ConnectionString {
|
|
|
806
851
|
}
|
|
807
852
|
}
|
|
808
853
|
defineS3(uri) {
|
|
809
|
-
this.bucket = uri.hostname;
|
|
810
|
-
this.accessKeyId = uri.username;
|
|
811
|
-
this.secretAccessKey = uri.password;
|
|
854
|
+
this.bucket = decodeURIComponent(uri.hostname);
|
|
855
|
+
this.accessKeyId = decodeURIComponent(uri.username);
|
|
856
|
+
this.secretAccessKey = decodeURIComponent(uri.password);
|
|
812
857
|
this.endpoint = S3_DEFAULT_ENDPOINT;
|
|
813
858
|
if (["/", "", null].includes(uri.pathname)) {
|
|
814
859
|
this.keyPrefix = "";
|
|
@@ -820,14 +865,14 @@ class ConnectionString {
|
|
|
820
865
|
defineMinio(uri) {
|
|
821
866
|
this.forcePathStyle = true;
|
|
822
867
|
this.endpoint = uri.origin;
|
|
823
|
-
this.accessKeyId = uri.username;
|
|
824
|
-
this.secretAccessKey = uri.password;
|
|
868
|
+
this.accessKeyId = decodeURIComponent(uri.username);
|
|
869
|
+
this.secretAccessKey = decodeURIComponent(uri.password);
|
|
825
870
|
if (["/", "", null].includes(uri.pathname)) {
|
|
826
871
|
this.bucket = "s3db";
|
|
827
872
|
this.keyPrefix = "";
|
|
828
873
|
} else {
|
|
829
874
|
let [, bucket, ...subpath] = uri.pathname.split("/");
|
|
830
|
-
this.bucket = bucket;
|
|
875
|
+
this.bucket = decodeURIComponent(bucket);
|
|
831
876
|
this.keyPrefix = [...subpath || []].join("/");
|
|
832
877
|
}
|
|
833
878
|
}
|
|
@@ -8836,18 +8881,83 @@ function getBehavior(behaviorName) {
|
|
|
8836
8881
|
const DEFAULT_BEHAVIOR = "user-management";
|
|
8837
8882
|
|
|
8838
8883
|
class Resource extends EventEmitter {
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8884
|
+
/**
|
|
8885
|
+
* Create a new Resource instance
|
|
8886
|
+
* @param {Object} config - Resource configuration
|
|
8887
|
+
* @param {string} config.name - Resource name
|
|
8888
|
+
* @param {Object} config.client - S3 client instance
|
|
8889
|
+
* @param {string} [config.version='v0'] - Resource version
|
|
8890
|
+
* @param {Object} [config.attributes={}] - Resource attributes schema
|
|
8891
|
+
* @param {string} [config.behavior='user-management'] - Resource behavior strategy
|
|
8892
|
+
* @param {string} [config.passphrase='secret'] - Encryption passphrase
|
|
8893
|
+
* @param {number} [config.parallelism=10] - Parallelism for bulk operations
|
|
8894
|
+
* @param {Array} [config.observers=[]] - Observer instances
|
|
8895
|
+
* @param {boolean} [config.cache=false] - Enable caching
|
|
8896
|
+
* @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
|
|
8897
|
+
* @param {boolean} [config.timestamps=false] - Enable automatic timestamps
|
|
8898
|
+
* @param {Object} [config.partitions={}] - Partition definitions
|
|
8899
|
+
* @param {boolean} [config.paranoid=true] - Security flag for dangerous operations
|
|
8900
|
+
* @param {boolean} [config.allNestedObjectsOptional=false] - Make nested objects optional
|
|
8901
|
+
* @param {Object} [config.hooks={}] - Custom hooks
|
|
8902
|
+
* @param {Object} [config.options={}] - Additional options
|
|
8903
|
+
* @example
|
|
8904
|
+
* const users = new Resource({
|
|
8905
|
+
* name: 'users',
|
|
8906
|
+
* client: s3Client,
|
|
8907
|
+
* attributes: {
|
|
8908
|
+
* name: 'string|required',
|
|
8909
|
+
* email: 'string|required',
|
|
8910
|
+
* password: 'secret|required'
|
|
8911
|
+
* },
|
|
8912
|
+
* behavior: 'user-management',
|
|
8913
|
+
* passphrase: 'my-secret-key',
|
|
8914
|
+
* timestamps: true,
|
|
8915
|
+
* partitions: {
|
|
8916
|
+
* byRegion: {
|
|
8917
|
+
* fields: { region: 'string' }
|
|
8918
|
+
* }
|
|
8919
|
+
* },
|
|
8920
|
+
* hooks: {
|
|
8921
|
+
* preInsert: [async (data) => {
|
|
8922
|
+
* console.log('Pre-insert hook:', data);
|
|
8923
|
+
* return data;
|
|
8924
|
+
* }]
|
|
8925
|
+
* }
|
|
8926
|
+
* });
|
|
8927
|
+
*/
|
|
8928
|
+
constructor(config) {
|
|
8850
8929
|
super();
|
|
8930
|
+
const validation = validateResourceConfig(config);
|
|
8931
|
+
if (!validation.isValid) {
|
|
8932
|
+
throw new Error(`Invalid Resource configuration:
|
|
8933
|
+
${validation.errors.join("\n")}`);
|
|
8934
|
+
}
|
|
8935
|
+
const {
|
|
8936
|
+
name,
|
|
8937
|
+
client,
|
|
8938
|
+
version = "1",
|
|
8939
|
+
attributes = {},
|
|
8940
|
+
behavior = DEFAULT_BEHAVIOR,
|
|
8941
|
+
passphrase = "secret",
|
|
8942
|
+
parallelism = 10,
|
|
8943
|
+
observers = [],
|
|
8944
|
+
cache = false,
|
|
8945
|
+
autoDecrypt = true,
|
|
8946
|
+
timestamps = false,
|
|
8947
|
+
partitions = {},
|
|
8948
|
+
paranoid = true,
|
|
8949
|
+
allNestedObjectsOptional = true,
|
|
8950
|
+
hooks = {},
|
|
8951
|
+
options = {}
|
|
8952
|
+
} = config;
|
|
8953
|
+
const mergedOptions = {
|
|
8954
|
+
cache: typeof options.cache === "boolean" ? options.cache : cache,
|
|
8955
|
+
autoDecrypt: typeof options.autoDecrypt === "boolean" ? options.autoDecrypt : autoDecrypt,
|
|
8956
|
+
timestamps: typeof options.timestamps === "boolean" ? options.timestamps : timestamps,
|
|
8957
|
+
paranoid: typeof options.paranoid === "boolean" ? options.paranoid : paranoid,
|
|
8958
|
+
allNestedObjectsOptional: typeof options.allNestedObjectsOptional === "boolean" ? options.allNestedObjectsOptional : allNestedObjectsOptional,
|
|
8959
|
+
partitions: options.partitions || partitions || {}
|
|
8960
|
+
};
|
|
8851
8961
|
this.name = name;
|
|
8852
8962
|
this.client = client;
|
|
8853
8963
|
this.version = version;
|
|
@@ -8855,15 +8965,14 @@ class Resource extends EventEmitter {
|
|
|
8855
8965
|
this.observers = observers;
|
|
8856
8966
|
this.parallelism = parallelism;
|
|
8857
8967
|
this.passphrase = passphrase ?? "secret";
|
|
8858
|
-
this.
|
|
8859
|
-
cache:
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
allNestedObjectsOptional:
|
|
8866
|
-
...options
|
|
8968
|
+
this.config = {
|
|
8969
|
+
cache: mergedOptions.cache,
|
|
8970
|
+
hooks,
|
|
8971
|
+
paranoid: mergedOptions.paranoid,
|
|
8972
|
+
timestamps: mergedOptions.timestamps,
|
|
8973
|
+
partitions: mergedOptions.partitions,
|
|
8974
|
+
autoDecrypt: mergedOptions.autoDecrypt,
|
|
8975
|
+
allNestedObjectsOptional: mergedOptions.allNestedObjectsOptional
|
|
8867
8976
|
};
|
|
8868
8977
|
this.hooks = {
|
|
8869
8978
|
preInsert: [],
|
|
@@ -8874,18 +8983,21 @@ class Resource extends EventEmitter {
|
|
|
8874
8983
|
afterDelete: []
|
|
8875
8984
|
};
|
|
8876
8985
|
this.attributes = attributes || {};
|
|
8877
|
-
if (
|
|
8986
|
+
if (this.config.timestamps) {
|
|
8878
8987
|
this.attributes.createdAt = "string|optional";
|
|
8879
8988
|
this.attributes.updatedAt = "string|optional";
|
|
8880
|
-
if (!this.
|
|
8881
|
-
this.
|
|
8989
|
+
if (!this.config.partitions) {
|
|
8990
|
+
this.config.partitions = {};
|
|
8991
|
+
}
|
|
8992
|
+
if (!this.config.partitions.byCreatedDate) {
|
|
8993
|
+
this.config.partitions.byCreatedDate = {
|
|
8882
8994
|
fields: {
|
|
8883
8995
|
createdAt: "date|maxlength:10"
|
|
8884
8996
|
}
|
|
8885
8997
|
};
|
|
8886
8998
|
}
|
|
8887
|
-
if (!this.
|
|
8888
|
-
this.
|
|
8999
|
+
if (!this.config.partitions.byUpdatedDate) {
|
|
9000
|
+
this.config.partitions.byUpdatedDate = {
|
|
8889
9001
|
fields: {
|
|
8890
9002
|
updatedAt: "date|maxlength:10"
|
|
8891
9003
|
}
|
|
@@ -8898,25 +9010,47 @@ class Resource extends EventEmitter {
|
|
|
8898
9010
|
passphrase,
|
|
8899
9011
|
version: this.version,
|
|
8900
9012
|
options: {
|
|
8901
|
-
|
|
8902
|
-
allNestedObjectsOptional: this.
|
|
9013
|
+
autoDecrypt: this.config.autoDecrypt,
|
|
9014
|
+
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
8903
9015
|
}
|
|
8904
9016
|
});
|
|
8905
|
-
this.validatePartitions();
|
|
8906
9017
|
this.setupPartitionHooks();
|
|
8907
|
-
|
|
8908
|
-
|
|
9018
|
+
this.validatePartitions();
|
|
9019
|
+
if (hooks) {
|
|
9020
|
+
for (const [event, hooksArr] of Object.entries(hooks)) {
|
|
8909
9021
|
if (Array.isArray(hooksArr) && this.hooks[event]) {
|
|
8910
9022
|
for (const fn of hooksArr) {
|
|
8911
|
-
|
|
9023
|
+
if (typeof fn === "function") {
|
|
9024
|
+
this.hooks[event].push(fn.bind(this));
|
|
9025
|
+
}
|
|
8912
9026
|
}
|
|
8913
9027
|
}
|
|
8914
9028
|
}
|
|
8915
9029
|
}
|
|
8916
9030
|
}
|
|
9031
|
+
/**
|
|
9032
|
+
* Get resource options (for backward compatibility with tests)
|
|
9033
|
+
*/
|
|
9034
|
+
get options() {
|
|
9035
|
+
return {
|
|
9036
|
+
timestamps: this.config.timestamps,
|
|
9037
|
+
partitions: this.config.partitions || {},
|
|
9038
|
+
cache: this.config.cache,
|
|
9039
|
+
autoDecrypt: this.config.autoDecrypt,
|
|
9040
|
+
paranoid: this.config.paranoid,
|
|
9041
|
+
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
9042
|
+
};
|
|
9043
|
+
}
|
|
8917
9044
|
export() {
|
|
8918
9045
|
const exported = this.schema.export();
|
|
8919
9046
|
exported.behavior = this.behavior;
|
|
9047
|
+
exported.timestamps = this.config.timestamps;
|
|
9048
|
+
exported.partitions = this.config.partitions || {};
|
|
9049
|
+
exported.paranoid = this.config.paranoid;
|
|
9050
|
+
exported.allNestedObjectsOptional = this.config.allNestedObjectsOptional;
|
|
9051
|
+
exported.autoDecrypt = this.config.autoDecrypt;
|
|
9052
|
+
exported.cache = this.config.cache;
|
|
9053
|
+
exported.hooks = this.hooks;
|
|
8920
9054
|
return exported;
|
|
8921
9055
|
}
|
|
8922
9056
|
/**
|
|
@@ -8926,18 +9060,21 @@ class Resource extends EventEmitter {
|
|
|
8926
9060
|
updateAttributes(newAttributes) {
|
|
8927
9061
|
const oldAttributes = this.attributes;
|
|
8928
9062
|
this.attributes = newAttributes;
|
|
8929
|
-
if (this.
|
|
9063
|
+
if (this.config.timestamps) {
|
|
8930
9064
|
newAttributes.createdAt = "string|optional";
|
|
8931
9065
|
newAttributes.updatedAt = "string|optional";
|
|
8932
|
-
if (!this.
|
|
8933
|
-
this.
|
|
9066
|
+
if (!this.config.partitions) {
|
|
9067
|
+
this.config.partitions = {};
|
|
9068
|
+
}
|
|
9069
|
+
if (!this.config.partitions.byCreatedDate) {
|
|
9070
|
+
this.config.partitions.byCreatedDate = {
|
|
8934
9071
|
fields: {
|
|
8935
9072
|
createdAt: "date|maxlength:10"
|
|
8936
9073
|
}
|
|
8937
9074
|
};
|
|
8938
9075
|
}
|
|
8939
|
-
if (!this.
|
|
8940
|
-
this.
|
|
9076
|
+
if (!this.config.partitions.byUpdatedDate) {
|
|
9077
|
+
this.config.partitions.byUpdatedDate = {
|
|
8941
9078
|
fields: {
|
|
8942
9079
|
updatedAt: "date|maxlength:10"
|
|
8943
9080
|
}
|
|
@@ -8949,10 +9086,13 @@ class Resource extends EventEmitter {
|
|
|
8949
9086
|
attributes: newAttributes,
|
|
8950
9087
|
passphrase: this.passphrase,
|
|
8951
9088
|
version: this.version,
|
|
8952
|
-
options:
|
|
9089
|
+
options: {
|
|
9090
|
+
autoDecrypt: this.config.autoDecrypt,
|
|
9091
|
+
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
9092
|
+
}
|
|
8953
9093
|
});
|
|
8954
|
-
this.validatePartitions();
|
|
8955
9094
|
this.setupPartitionHooks();
|
|
9095
|
+
this.validatePartitions();
|
|
8956
9096
|
return { oldAttributes, newAttributes };
|
|
8957
9097
|
}
|
|
8958
9098
|
/**
|
|
@@ -8983,15 +9123,24 @@ class Resource extends EventEmitter {
|
|
|
8983
9123
|
* Setup automatic partition hooks
|
|
8984
9124
|
*/
|
|
8985
9125
|
setupPartitionHooks() {
|
|
8986
|
-
|
|
8987
|
-
if (!partitions || Object.keys(partitions).length === 0) {
|
|
9126
|
+
if (!this.config.partitions) {
|
|
8988
9127
|
return;
|
|
8989
9128
|
}
|
|
8990
|
-
|
|
9129
|
+
const partitions = this.config.partitions;
|
|
9130
|
+
if (Object.keys(partitions).length === 0) {
|
|
9131
|
+
return;
|
|
9132
|
+
}
|
|
9133
|
+
if (!this.hooks.afterInsert) {
|
|
9134
|
+
this.hooks.afterInsert = [];
|
|
9135
|
+
}
|
|
9136
|
+
this.hooks.afterInsert.push(async (data) => {
|
|
8991
9137
|
await this.createPartitionReferences(data);
|
|
8992
9138
|
return data;
|
|
8993
9139
|
});
|
|
8994
|
-
this.
|
|
9140
|
+
if (!this.hooks.afterDelete) {
|
|
9141
|
+
this.hooks.afterDelete = [];
|
|
9142
|
+
}
|
|
9143
|
+
this.hooks.afterDelete.push(async (data) => {
|
|
8995
9144
|
await this.deletePartitionReferences(data);
|
|
8996
9145
|
return data;
|
|
8997
9146
|
});
|
|
@@ -9016,8 +9165,11 @@ class Resource extends EventEmitter {
|
|
|
9016
9165
|
* @throws {Error} If partition fields don't exist in current schema
|
|
9017
9166
|
*/
|
|
9018
9167
|
validatePartitions() {
|
|
9019
|
-
|
|
9020
|
-
|
|
9168
|
+
if (!this.config.partitions) {
|
|
9169
|
+
return;
|
|
9170
|
+
}
|
|
9171
|
+
const partitions = this.config.partitions;
|
|
9172
|
+
if (Object.keys(partitions).length === 0) {
|
|
9021
9173
|
return;
|
|
9022
9174
|
}
|
|
9023
9175
|
const currentAttributes = Object.keys(this.attributes || {});
|
|
@@ -9124,10 +9276,10 @@ class Resource extends EventEmitter {
|
|
|
9124
9276
|
* // Returns: null
|
|
9125
9277
|
*/
|
|
9126
9278
|
getPartitionKey({ partitionName, id, data }) {
|
|
9127
|
-
|
|
9128
|
-
if (!partition) {
|
|
9279
|
+
if (!this.config.partitions || !this.config.partitions[partitionName]) {
|
|
9129
9280
|
throw new Error(`Partition '${partitionName}' not found`);
|
|
9130
9281
|
}
|
|
9282
|
+
const partition = this.config.partitions[partitionName];
|
|
9131
9283
|
const partitionSegments = [];
|
|
9132
9284
|
const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
9133
9285
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -9183,6 +9335,12 @@ class Resource extends EventEmitter {
|
|
|
9183
9335
|
* name: 'Jane Smith',
|
|
9184
9336
|
* email: 'jane@example.com'
|
|
9185
9337
|
* });
|
|
9338
|
+
*
|
|
9339
|
+
* // Insert with auto-generated password for secret field
|
|
9340
|
+
* const user = await resource.insert({
|
|
9341
|
+
* name: 'John Doe',
|
|
9342
|
+
* email: 'john@example.com',
|
|
9343
|
+
* });
|
|
9186
9344
|
*/
|
|
9187
9345
|
async insert({ id, ...attributes }) {
|
|
9188
9346
|
if (this.options.timestamps) {
|
|
@@ -9278,7 +9436,7 @@ class Resource extends EventEmitter {
|
|
|
9278
9436
|
passphrase: this.passphrase,
|
|
9279
9437
|
version: objectVersion,
|
|
9280
9438
|
options: {
|
|
9281
|
-
...this.
|
|
9439
|
+
...this.config,
|
|
9282
9440
|
autoDecrypt: false,
|
|
9283
9441
|
// Disable decryption
|
|
9284
9442
|
autoEncrypt: false
|
|
@@ -9365,7 +9523,7 @@ class Resource extends EventEmitter {
|
|
|
9365
9523
|
*/
|
|
9366
9524
|
async update(id, attributes) {
|
|
9367
9525
|
const live = await this.get(id);
|
|
9368
|
-
if (this.
|
|
9526
|
+
if (this.config.timestamps) {
|
|
9369
9527
|
attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9370
9528
|
}
|
|
9371
9529
|
const preProcessedData = await this.executeHooks("preUpdate", attributes);
|
|
@@ -9487,7 +9645,7 @@ class Resource extends EventEmitter {
|
|
|
9487
9645
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
9488
9646
|
let prefix;
|
|
9489
9647
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
9490
|
-
const partitionDef = this.
|
|
9648
|
+
const partitionDef = this.config.partitions[partition];
|
|
9491
9649
|
if (!partitionDef) {
|
|
9492
9650
|
throw new Error(`Partition '${partition}' not found`);
|
|
9493
9651
|
}
|
|
@@ -9572,9 +9730,9 @@ class Resource extends EventEmitter {
|
|
|
9572
9730
|
return results;
|
|
9573
9731
|
}
|
|
9574
9732
|
async deleteAll() {
|
|
9575
|
-
if (this.
|
|
9733
|
+
if (this.config.paranoid !== false) {
|
|
9576
9734
|
throw new Error(
|
|
9577
|
-
`deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.
|
|
9735
|
+
`deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
|
|
9578
9736
|
);
|
|
9579
9737
|
}
|
|
9580
9738
|
const prefix = `resource=${this.name}/v=${this.version}`;
|
|
@@ -9591,9 +9749,9 @@ class Resource extends EventEmitter {
|
|
|
9591
9749
|
* @returns {Promise<Object>} Deletion report
|
|
9592
9750
|
*/
|
|
9593
9751
|
async deleteAllData() {
|
|
9594
|
-
if (this.
|
|
9752
|
+
if (this.config.paranoid !== false) {
|
|
9595
9753
|
throw new Error(
|
|
9596
|
-
`deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.
|
|
9754
|
+
`deleteAllData() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
|
|
9597
9755
|
);
|
|
9598
9756
|
}
|
|
9599
9757
|
const prefix = `resource=${this.name}`;
|
|
@@ -9636,10 +9794,10 @@ class Resource extends EventEmitter {
|
|
|
9636
9794
|
async listIds({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
9637
9795
|
let prefix;
|
|
9638
9796
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
9639
|
-
|
|
9640
|
-
if (!partitionDef) {
|
|
9797
|
+
if (!this.config.partitions || !this.config.partitions[partition]) {
|
|
9641
9798
|
throw new Error(`Partition '${partition}' not found`);
|
|
9642
9799
|
}
|
|
9800
|
+
const partitionDef = this.config.partitions[partition];
|
|
9643
9801
|
const partitionSegments = [];
|
|
9644
9802
|
const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
9645
9803
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -9730,10 +9888,10 @@ class Resource extends EventEmitter {
|
|
|
9730
9888
|
this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
|
|
9731
9889
|
return validResults2;
|
|
9732
9890
|
}
|
|
9733
|
-
|
|
9734
|
-
if (!partitionDef) {
|
|
9891
|
+
if (!this.config.partitions || !this.config.partitions[partition]) {
|
|
9735
9892
|
throw new Error(`Partition '${partition}' not found`);
|
|
9736
9893
|
}
|
|
9894
|
+
const partitionDef = this.config.partitions[partition];
|
|
9737
9895
|
const partitionSegments = [];
|
|
9738
9896
|
const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
9739
9897
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -10038,16 +10196,14 @@ class Resource extends EventEmitter {
|
|
|
10038
10196
|
}
|
|
10039
10197
|
/**
|
|
10040
10198
|
* Generate definition hash for this resource
|
|
10041
|
-
* @returns {string} SHA256 hash of the
|
|
10199
|
+
* @returns {string} SHA256 hash of the resource definition (name + attributes)
|
|
10042
10200
|
*/
|
|
10043
10201
|
getDefinitionHash() {
|
|
10044
|
-
const
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
}
|
|
10050
|
-
const stableString = jsonStableStringify(stableAttributes);
|
|
10202
|
+
const definition = {
|
|
10203
|
+
attributes: this.attributes,
|
|
10204
|
+
behavior: this.behavior
|
|
10205
|
+
};
|
|
10206
|
+
const stableString = jsonStableStringify(definition);
|
|
10051
10207
|
return `sha256:${createHash("sha256").update(stableString).digest("hex")}`;
|
|
10052
10208
|
}
|
|
10053
10209
|
/**
|
|
@@ -10076,7 +10232,7 @@ class Resource extends EventEmitter {
|
|
|
10076
10232
|
passphrase: this.passphrase,
|
|
10077
10233
|
version,
|
|
10078
10234
|
options: {
|
|
10079
|
-
...this.
|
|
10235
|
+
...this.config,
|
|
10080
10236
|
// For older versions, be more lenient with decryption
|
|
10081
10237
|
autoDecrypt: true,
|
|
10082
10238
|
autoEncrypt: true
|
|
@@ -10093,7 +10249,7 @@ class Resource extends EventEmitter {
|
|
|
10093
10249
|
* @param {Object} data - Inserted object data
|
|
10094
10250
|
*/
|
|
10095
10251
|
async createPartitionReferences(data) {
|
|
10096
|
-
const partitions = this.
|
|
10252
|
+
const partitions = this.config.partitions;
|
|
10097
10253
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
10098
10254
|
return;
|
|
10099
10255
|
}
|
|
@@ -10124,7 +10280,7 @@ class Resource extends EventEmitter {
|
|
|
10124
10280
|
* @param {Object} data - Deleted object data
|
|
10125
10281
|
*/
|
|
10126
10282
|
async deletePartitionReferences(data) {
|
|
10127
|
-
const partitions = this.
|
|
10283
|
+
const partitions = this.config.partitions;
|
|
10128
10284
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
10129
10285
|
return;
|
|
10130
10286
|
}
|
|
@@ -10216,7 +10372,7 @@ class Resource extends EventEmitter {
|
|
|
10216
10372
|
* @param {Object} data - Updated object data
|
|
10217
10373
|
*/
|
|
10218
10374
|
async updatePartitionReferences(data) {
|
|
10219
|
-
const partitions = this.
|
|
10375
|
+
const partitions = this.config.partitions;
|
|
10220
10376
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
10221
10377
|
return;
|
|
10222
10378
|
}
|
|
@@ -10272,10 +10428,10 @@ class Resource extends EventEmitter {
|
|
|
10272
10428
|
* });
|
|
10273
10429
|
*/
|
|
10274
10430
|
async getFromPartition({ id, partitionName, partitionValues = {} }) {
|
|
10275
|
-
|
|
10276
|
-
if (!partition) {
|
|
10431
|
+
if (!this.config.partitions || !this.config.partitions[partitionName]) {
|
|
10277
10432
|
throw new Error(`Partition '${partitionName}' not found`);
|
|
10278
10433
|
}
|
|
10434
|
+
const partition = this.config.partitions[partitionName];
|
|
10279
10435
|
const partitionSegments = [];
|
|
10280
10436
|
const sortedFields = Object.entries(partition.fields).sort(([a], [b]) => a.localeCompare(b));
|
|
10281
10437
|
for (const [fieldName, rule] of sortedFields) {
|
|
@@ -10323,6 +10479,98 @@ class Resource extends EventEmitter {
|
|
|
10323
10479
|
return data;
|
|
10324
10480
|
}
|
|
10325
10481
|
}
|
|
10482
|
+
function validateResourceConfig(config) {
|
|
10483
|
+
const errors = [];
|
|
10484
|
+
if (!config.name) {
|
|
10485
|
+
errors.push("Resource 'name' is required");
|
|
10486
|
+
} else if (typeof config.name !== "string") {
|
|
10487
|
+
errors.push("Resource 'name' must be a string");
|
|
10488
|
+
} else if (config.name.trim() === "") {
|
|
10489
|
+
errors.push("Resource 'name' cannot be empty");
|
|
10490
|
+
}
|
|
10491
|
+
if (!config.client) {
|
|
10492
|
+
errors.push("S3 'client' is required");
|
|
10493
|
+
}
|
|
10494
|
+
if (!config.attributes) {
|
|
10495
|
+
errors.push("Resource 'attributes' are required");
|
|
10496
|
+
} else if (typeof config.attributes !== "object" || Array.isArray(config.attributes)) {
|
|
10497
|
+
errors.push("Resource 'attributes' must be an object");
|
|
10498
|
+
} else if (Object.keys(config.attributes).length === 0) {
|
|
10499
|
+
errors.push("Resource 'attributes' cannot be empty");
|
|
10500
|
+
}
|
|
10501
|
+
if (config.version !== void 0 && typeof config.version !== "string") {
|
|
10502
|
+
errors.push("Resource 'version' must be a string");
|
|
10503
|
+
}
|
|
10504
|
+
if (config.behavior !== void 0 && typeof config.behavior !== "string") {
|
|
10505
|
+
errors.push("Resource 'behavior' must be a string");
|
|
10506
|
+
}
|
|
10507
|
+
if (config.passphrase !== void 0 && typeof config.passphrase !== "string") {
|
|
10508
|
+
errors.push("Resource 'passphrase' must be a string");
|
|
10509
|
+
}
|
|
10510
|
+
if (config.parallelism !== void 0) {
|
|
10511
|
+
if (typeof config.parallelism !== "number" || !Number.isInteger(config.parallelism)) {
|
|
10512
|
+
errors.push("Resource 'parallelism' must be an integer");
|
|
10513
|
+
} else if (config.parallelism < 1) {
|
|
10514
|
+
errors.push("Resource 'parallelism' must be greater than 0");
|
|
10515
|
+
}
|
|
10516
|
+
}
|
|
10517
|
+
if (config.observers !== void 0 && !Array.isArray(config.observers)) {
|
|
10518
|
+
errors.push("Resource 'observers' must be an array");
|
|
10519
|
+
}
|
|
10520
|
+
const booleanFields = ["cache", "autoDecrypt", "timestamps", "paranoid", "allNestedObjectsOptional"];
|
|
10521
|
+
for (const field of booleanFields) {
|
|
10522
|
+
if (config[field] !== void 0 && typeof config[field] !== "boolean") {
|
|
10523
|
+
errors.push(`Resource '${field}' must be a boolean`);
|
|
10524
|
+
}
|
|
10525
|
+
}
|
|
10526
|
+
if (config.partitions !== void 0) {
|
|
10527
|
+
if (typeof config.partitions !== "object" || Array.isArray(config.partitions)) {
|
|
10528
|
+
errors.push("Resource 'partitions' must be an object");
|
|
10529
|
+
} else {
|
|
10530
|
+
for (const [partitionName, partitionDef] of Object.entries(config.partitions)) {
|
|
10531
|
+
if (typeof partitionDef !== "object" || Array.isArray(partitionDef)) {
|
|
10532
|
+
errors.push(`Partition '${partitionName}' must be an object`);
|
|
10533
|
+
} else if (!partitionDef.fields) {
|
|
10534
|
+
errors.push(`Partition '${partitionName}' must have a 'fields' property`);
|
|
10535
|
+
} else if (typeof partitionDef.fields !== "object" || Array.isArray(partitionDef.fields)) {
|
|
10536
|
+
errors.push(`Partition '${partitionName}.fields' must be an object`);
|
|
10537
|
+
} else {
|
|
10538
|
+
for (const [fieldName, fieldType] of Object.entries(partitionDef.fields)) {
|
|
10539
|
+
if (typeof fieldType !== "string") {
|
|
10540
|
+
errors.push(`Partition '${partitionName}.fields.${fieldName}' must be a string`);
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
10546
|
+
}
|
|
10547
|
+
if (config.hooks !== void 0) {
|
|
10548
|
+
if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
|
|
10549
|
+
errors.push("Resource 'hooks' must be an object");
|
|
10550
|
+
} else {
|
|
10551
|
+
const validHookEvents = ["preInsert", "afterInsert", "preUpdate", "afterUpdate", "preDelete", "afterDelete"];
|
|
10552
|
+
for (const [event, hooksArr] of Object.entries(config.hooks)) {
|
|
10553
|
+
if (!validHookEvents.includes(event)) {
|
|
10554
|
+
errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
|
|
10555
|
+
} else if (!Array.isArray(hooksArr)) {
|
|
10556
|
+
errors.push(`Resource 'hooks.${event}' must be an array`);
|
|
10557
|
+
} else {
|
|
10558
|
+
for (let i = 0; i < hooksArr.length; i++) {
|
|
10559
|
+
const hook = hooksArr[i];
|
|
10560
|
+
if (typeof hook !== "function") {
|
|
10561
|
+
if (typeof hook === "string") continue;
|
|
10562
|
+
continue;
|
|
10563
|
+
}
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
return {
|
|
10570
|
+
isValid: errors.length === 0,
|
|
10571
|
+
errors
|
|
10572
|
+
};
|
|
10573
|
+
}
|
|
10326
10574
|
|
|
10327
10575
|
class Database extends EventEmitter {
|
|
10328
10576
|
constructor(options) {
|
|
@@ -10330,7 +10578,7 @@ class Database extends EventEmitter {
|
|
|
10330
10578
|
this.version = "1";
|
|
10331
10579
|
this.s3dbVersion = (() => {
|
|
10332
10580
|
try {
|
|
10333
|
-
return true ? "4.1.
|
|
10581
|
+
return true ? "4.1.12" : "latest";
|
|
10334
10582
|
} catch (e) {
|
|
10335
10583
|
return "latest";
|
|
10336
10584
|
}
|
|
@@ -10343,10 +10591,21 @@ class Database extends EventEmitter {
|
|
|
10343
10591
|
this.plugins = options.plugins || [];
|
|
10344
10592
|
this.cache = options.cache;
|
|
10345
10593
|
this.passphrase = options.passphrase || "secret";
|
|
10594
|
+
let connectionString = options.connectionString;
|
|
10595
|
+
if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
|
|
10596
|
+
connectionString = ConnectionString.buildFromParams({
|
|
10597
|
+
bucket: options.bucket,
|
|
10598
|
+
region: options.region,
|
|
10599
|
+
accessKeyId: options.accessKeyId,
|
|
10600
|
+
secretAccessKey: options.secretAccessKey,
|
|
10601
|
+
endpoint: options.endpoint,
|
|
10602
|
+
forcePathStyle: options.forcePathStyle
|
|
10603
|
+
});
|
|
10604
|
+
}
|
|
10346
10605
|
this.client = options.client || new Client({
|
|
10347
10606
|
verbose: this.verbose,
|
|
10348
10607
|
parallelism: this.parallelism,
|
|
10349
|
-
connectionString
|
|
10608
|
+
connectionString
|
|
10350
10609
|
});
|
|
10351
10610
|
this.bucket = this.client.bucket;
|
|
10352
10611
|
this.keyPrefix = this.client.keyPrefix;
|
|
@@ -10371,15 +10630,18 @@ class Database extends EventEmitter {
|
|
|
10371
10630
|
name,
|
|
10372
10631
|
client: this.client,
|
|
10373
10632
|
version: currentVersion,
|
|
10374
|
-
options: {
|
|
10375
|
-
...versionData.options,
|
|
10376
|
-
partitions: resourceMetadata.partitions || versionData.options?.partitions || {}
|
|
10377
|
-
},
|
|
10378
10633
|
attributes: versionData.attributes,
|
|
10379
10634
|
behavior: versionData.behavior || "user-management",
|
|
10380
10635
|
parallelism: this.parallelism,
|
|
10381
10636
|
passphrase: this.passphrase,
|
|
10382
|
-
observers: [this]
|
|
10637
|
+
observers: [this],
|
|
10638
|
+
cache: this.cache,
|
|
10639
|
+
timestamps: versionData.options?.timestamps || false,
|
|
10640
|
+
partitions: resourceMetadata.partitions || versionData.options?.partitions || {},
|
|
10641
|
+
paranoid: versionData.options?.paranoid !== false,
|
|
10642
|
+
allNestedObjectsOptional: versionData.options?.allNestedObjectsOptional || false,
|
|
10643
|
+
autoDecrypt: versionData.options?.autoDecrypt !== false,
|
|
10644
|
+
hooks: versionData.options?.hooks || {}
|
|
10383
10645
|
});
|
|
10384
10646
|
}
|
|
10385
10647
|
}
|
|
@@ -10448,7 +10710,7 @@ class Database extends EventEmitter {
|
|
|
10448
10710
|
generateDefinitionHash(definition, behavior = void 0) {
|
|
10449
10711
|
const attributes = definition.attributes;
|
|
10450
10712
|
const stableAttributes = { ...attributes };
|
|
10451
|
-
if (definition.
|
|
10713
|
+
if (definition.timestamps) {
|
|
10452
10714
|
delete stableAttributes.createdAt;
|
|
10453
10715
|
delete stableAttributes.updatedAt;
|
|
10454
10716
|
}
|
|
@@ -10510,14 +10772,22 @@ class Database extends EventEmitter {
|
|
|
10510
10772
|
}
|
|
10511
10773
|
metadata.resources[name] = {
|
|
10512
10774
|
currentVersion: version,
|
|
10513
|
-
partitions:
|
|
10775
|
+
partitions: resource.config.partitions || {},
|
|
10514
10776
|
versions: {
|
|
10515
10777
|
...existingResource?.versions,
|
|
10516
10778
|
// Preserve previous versions
|
|
10517
10779
|
[version]: {
|
|
10518
10780
|
hash: definitionHash,
|
|
10519
10781
|
attributes: resourceDef.attributes,
|
|
10520
|
-
options:
|
|
10782
|
+
options: {
|
|
10783
|
+
timestamps: resource.config.timestamps,
|
|
10784
|
+
partitions: resource.config.partitions,
|
|
10785
|
+
paranoid: resource.config.paranoid,
|
|
10786
|
+
allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
|
|
10787
|
+
autoDecrypt: resource.config.autoDecrypt,
|
|
10788
|
+
cache: resource.config.cache,
|
|
10789
|
+
hooks: resourceDef.hooks || {}
|
|
10790
|
+
},
|
|
10521
10791
|
behavior: resourceDef.behavior || "user-management",
|
|
10522
10792
|
createdAt: isNewVersion ? (/* @__PURE__ */ new Date()).toISOString() : existingVersionData?.createdAt
|
|
10523
10793
|
}
|
|
@@ -10570,10 +10840,9 @@ class Database extends EventEmitter {
|
|
|
10570
10840
|
observers: [],
|
|
10571
10841
|
client: this.client,
|
|
10572
10842
|
version: "temp",
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
}
|
|
10843
|
+
passphrase: this.passphrase,
|
|
10844
|
+
cache: this.cache,
|
|
10845
|
+
...options
|
|
10577
10846
|
});
|
|
10578
10847
|
const newHash = this.generateDefinitionHash(tempResource.export(), behavior);
|
|
10579
10848
|
const existingHash = this.generateDefinitionHash(this.resources[name].export(), this.resources[name].behavior);
|
|
@@ -10613,7 +10882,7 @@ class Database extends EventEmitter {
|
|
|
10613
10882
|
async createResource({ name, attributes, options = {}, behavior = "user-management" }) {
|
|
10614
10883
|
if (this.resources[name]) {
|
|
10615
10884
|
const existingResource = this.resources[name];
|
|
10616
|
-
Object.assign(existingResource.
|
|
10885
|
+
Object.assign(existingResource.config, {
|
|
10617
10886
|
cache: this.cache,
|
|
10618
10887
|
...options
|
|
10619
10888
|
});
|
|
@@ -10640,10 +10909,9 @@ class Database extends EventEmitter {
|
|
|
10640
10909
|
observers: [this],
|
|
10641
10910
|
client: this.client,
|
|
10642
10911
|
version,
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
}
|
|
10912
|
+
passphrase: this.passphrase,
|
|
10913
|
+
cache: this.cache,
|
|
10914
|
+
...options
|
|
10647
10915
|
});
|
|
10648
10916
|
this.resources[name] = resource;
|
|
10649
10917
|
await this.uploadMetadataFile();
|
|
@@ -17345,4 +17613,4 @@ class CachePlugin extends Plugin {
|
|
|
17345
17613
|
}
|
|
17346
17614
|
}
|
|
17347
17615
|
|
|
17348
|
-
export { BaseError, Cache, CachePlugin, Client, ConnectionString, CostsPlugin, Database, ErrorMap, InvalidResourceItem, MemoryCache, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, Plugin, PluginObject, ResourceIdsPageReader, ResourceIdsReader, ResourceReader, ResourceWriter, S3Cache, S3_DEFAULT_ENDPOINT, S3_DEFAULT_REGION, S3db, UnknownError, Validator, ValidatorManager, decrypt, encrypt, sha256, streamToString };
|
|
17616
|
+
export { AuthenticationError, BaseError, Cache, CachePlugin, Client, ConnectionString, CostsPlugin, Database, DatabaseError, EncryptionError, ErrorMap, InvalidResourceItem, MemoryCache, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PermissionError, Plugin, PluginObject, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceNotFound as ResourceNotFoundError, ResourceReader, ResourceWriter, S3Cache, S3db as S3DB, S3DBError, S3_DEFAULT_ENDPOINT, S3_DEFAULT_REGION, S3db, S3DBError as S3dbError, UnknownError, ValidationError, Validator, ValidatorManager, decrypt, S3db as default, encrypt, sha256, streamToString };
|