s3db.js 13.6.1 → 14.0.2
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 +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +85 -50
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and comprehensive optimal K analysis using multiple metrics.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { ValidationError } from '../../errors.js';
|
|
8
9
|
import { euclideanDistance } from './distances.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -31,15 +32,27 @@ export function kmeans(vectors, k, options = {}) {
|
|
|
31
32
|
} = options;
|
|
32
33
|
|
|
33
34
|
if (vectors.length === 0) {
|
|
34
|
-
throw new
|
|
35
|
+
throw new ValidationError('Cannot cluster empty vector array', {
|
|
36
|
+
operation: 'kmeans',
|
|
37
|
+
retriable: false,
|
|
38
|
+
suggestion: 'Provide at least one vector before invoking k-means.'
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
if (k < 1) {
|
|
38
|
-
throw new
|
|
43
|
+
throw new ValidationError(`k must be at least 1, got ${k}`, {
|
|
44
|
+
operation: 'kmeans',
|
|
45
|
+
retriable: false,
|
|
46
|
+
suggestion: 'Use a positive integer for the number of clusters.'
|
|
47
|
+
});
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
if (k > vectors.length) {
|
|
42
|
-
throw new
|
|
51
|
+
throw new ValidationError(`k (${k}) cannot be greater than number of vectors (${vectors.length})`, {
|
|
52
|
+
operation: 'kmeans',
|
|
53
|
+
retriable: false,
|
|
54
|
+
suggestion: 'Reduce k or provide more vectors before clustering.'
|
|
55
|
+
});
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
const dimensions = vectors[0].length;
|
|
@@ -47,7 +60,16 @@ export function kmeans(vectors, k, options = {}) {
|
|
|
47
60
|
// Validate all vectors have same dimensions
|
|
48
61
|
for (let i = 1; i < vectors.length; i++) {
|
|
49
62
|
if (vectors[i].length !== dimensions) {
|
|
50
|
-
throw new
|
|
63
|
+
throw new ValidationError('All vectors must have same dimensions.', {
|
|
64
|
+
operation: 'kmeans',
|
|
65
|
+
retriable: false,
|
|
66
|
+
suggestion: 'Pad or trim vectors so every row has identical dimensionality.',
|
|
67
|
+
metadata: {
|
|
68
|
+
expectedDimensions: dimensions,
|
|
69
|
+
actualDimensions: vectors[i].length,
|
|
70
|
+
index: i
|
|
71
|
+
}
|
|
72
|
+
});
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
|
package/src/resource.class.js
CHANGED
|
@@ -191,6 +191,15 @@ export class Resource extends AsyncEventEmitter {
|
|
|
191
191
|
createdBy,
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
+
// Store raw schema definition (accessible as resource.$schema)
|
|
195
|
+
// This is the LITERAL object passed to createResource()
|
|
196
|
+
// Useful for plugins, documentation, and introspection
|
|
197
|
+
this.$schema = cloneDeep(config);
|
|
198
|
+
|
|
199
|
+
// Add metadata timestamps
|
|
200
|
+
this.$schema._createdAt = Date.now();
|
|
201
|
+
this.$schema._updatedAt = Date.now();
|
|
202
|
+
|
|
194
203
|
// Initialize hooks system - expanded to cover ALL methods
|
|
195
204
|
this.hooks = {
|
|
196
205
|
// Insert hooks
|
|
@@ -1175,7 +1184,14 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1175
1184
|
// LOG: body e contentType antes do putObject
|
|
1176
1185
|
// Only throw if behavior is 'body-only' and body is empty
|
|
1177
1186
|
if (this.behavior === 'body-only' && (!body || body === "")) {
|
|
1178
|
-
throw new
|
|
1187
|
+
throw new ResourceError('Body required for body-only behavior', {
|
|
1188
|
+
resourceName: this.name,
|
|
1189
|
+
operation: 'insert',
|
|
1190
|
+
id: finalId,
|
|
1191
|
+
statusCode: 400,
|
|
1192
|
+
retriable: false,
|
|
1193
|
+
suggestion: 'Include a request body when using behavior "body-only" or switch to "body-overflow".'
|
|
1194
|
+
});
|
|
1179
1195
|
}
|
|
1180
1196
|
// For other behaviors, allow empty body (all data in metadata)
|
|
1181
1197
|
|
|
@@ -1272,8 +1288,22 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1272
1288
|
* const user = await resource.get('user-123');
|
|
1273
1289
|
*/
|
|
1274
1290
|
async get(id) {
|
|
1275
|
-
if (isObject(id))
|
|
1276
|
-
|
|
1291
|
+
if (isObject(id)) {
|
|
1292
|
+
throw new ValidationError('Resource id must be a string', {
|
|
1293
|
+
field: 'id',
|
|
1294
|
+
statusCode: 400,
|
|
1295
|
+
retriable: false,
|
|
1296
|
+
suggestion: 'Pass the resource id as a string value (e.g. "user-123").'
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
if (isEmpty(id)) {
|
|
1300
|
+
throw new ValidationError('Resource id cannot be empty', {
|
|
1301
|
+
field: 'id',
|
|
1302
|
+
statusCode: 400,
|
|
1303
|
+
retriable: false,
|
|
1304
|
+
suggestion: 'Provide a non-empty id when calling resource methods.'
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1277
1307
|
|
|
1278
1308
|
// Execute beforeGet hooks
|
|
1279
1309
|
await this.executeHooks('beforeGet', { id });
|
|
@@ -1454,12 +1484,23 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1454
1484
|
*/
|
|
1455
1485
|
async update(id, attributes) {
|
|
1456
1486
|
if (isEmpty(id)) {
|
|
1457
|
-
throw new
|
|
1487
|
+
throw new ValidationError('Resource id cannot be empty', {
|
|
1488
|
+
field: 'id',
|
|
1489
|
+
statusCode: 400,
|
|
1490
|
+
retriable: false,
|
|
1491
|
+
suggestion: 'Provide the target id when calling update().'
|
|
1492
|
+
});
|
|
1458
1493
|
}
|
|
1459
1494
|
// Garante que o recurso existe antes de atualizar
|
|
1460
1495
|
const exists = await this.exists(id);
|
|
1461
1496
|
if (!exists) {
|
|
1462
|
-
throw new
|
|
1497
|
+
throw new ResourceError(`Resource with id '${id}' does not exist`, {
|
|
1498
|
+
resourceName: this.name,
|
|
1499
|
+
id,
|
|
1500
|
+
statusCode: 404,
|
|
1501
|
+
retriable: false,
|
|
1502
|
+
suggestion: 'Ensure the record exists or create it before attempting an update.'
|
|
1503
|
+
});
|
|
1463
1504
|
}
|
|
1464
1505
|
const originalData = await this.get(id);
|
|
1465
1506
|
const attributesClone = cloneDeep(attributes);
|
|
@@ -1685,11 +1726,21 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1685
1726
|
*/
|
|
1686
1727
|
async patch(id, fields, options = {}) {
|
|
1687
1728
|
if (isEmpty(id)) {
|
|
1688
|
-
throw new
|
|
1729
|
+
throw new ValidationError('Resource id cannot be empty', {
|
|
1730
|
+
field: 'id',
|
|
1731
|
+
statusCode: 400,
|
|
1732
|
+
retriable: false,
|
|
1733
|
+
suggestion: 'Provide the target id when calling patch().'
|
|
1734
|
+
});
|
|
1689
1735
|
}
|
|
1690
1736
|
|
|
1691
1737
|
if (!fields || typeof fields !== 'object') {
|
|
1692
|
-
throw new
|
|
1738
|
+
throw new ValidationError('fields must be a non-empty object', {
|
|
1739
|
+
field: 'fields',
|
|
1740
|
+
statusCode: 400,
|
|
1741
|
+
retriable: false,
|
|
1742
|
+
suggestion: 'Pass a plain object with the fields to update (e.g. { status: "active" }).'
|
|
1743
|
+
});
|
|
1693
1744
|
}
|
|
1694
1745
|
|
|
1695
1746
|
// Execute beforePatch hooks
|
|
@@ -1843,11 +1894,21 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1843
1894
|
*/
|
|
1844
1895
|
async replace(id, fullData, options = {}) {
|
|
1845
1896
|
if (isEmpty(id)) {
|
|
1846
|
-
throw new
|
|
1897
|
+
throw new ValidationError('Resource id cannot be empty', {
|
|
1898
|
+
field: 'id',
|
|
1899
|
+
statusCode: 400,
|
|
1900
|
+
retriable: false,
|
|
1901
|
+
suggestion: 'Provide the target id when calling replace().'
|
|
1902
|
+
});
|
|
1847
1903
|
}
|
|
1848
1904
|
|
|
1849
1905
|
if (!fullData || typeof fullData !== 'object') {
|
|
1850
|
-
throw new
|
|
1906
|
+
throw new ValidationError('fullData must be a non-empty object', {
|
|
1907
|
+
field: 'fullData',
|
|
1908
|
+
statusCode: 400,
|
|
1909
|
+
retriable: false,
|
|
1910
|
+
suggestion: 'Pass a plain object containing the full resource payload to replace().'
|
|
1911
|
+
});
|
|
1851
1912
|
}
|
|
1852
1913
|
|
|
1853
1914
|
// Execute beforeReplace hooks
|
|
@@ -1921,7 +1982,14 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1921
1982
|
|
|
1922
1983
|
// Only throw if behavior is 'body-only' and body is empty
|
|
1923
1984
|
if (this.behavior === 'body-only' && (!body || body === "")) {
|
|
1924
|
-
throw new
|
|
1985
|
+
throw new ResourceError('Body required for body-only behavior', {
|
|
1986
|
+
resourceName: this.name,
|
|
1987
|
+
operation: 'replace',
|
|
1988
|
+
id,
|
|
1989
|
+
statusCode: 400,
|
|
1990
|
+
retriable: false,
|
|
1991
|
+
suggestion: 'Include a request body when using behavior "body-only" or switch to "body-overflow".'
|
|
1992
|
+
});
|
|
1925
1993
|
}
|
|
1926
1994
|
|
|
1927
1995
|
// Store to S3 (overwrites if exists, creates if not - true replace/upsert)
|
|
@@ -2000,12 +2068,22 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2000
2068
|
*/
|
|
2001
2069
|
async updateConditional(id, attributes, options = {}) {
|
|
2002
2070
|
if (isEmpty(id)) {
|
|
2003
|
-
throw new
|
|
2071
|
+
throw new ValidationError('Resource id cannot be empty', {
|
|
2072
|
+
field: 'id',
|
|
2073
|
+
statusCode: 400,
|
|
2074
|
+
retriable: false,
|
|
2075
|
+
suggestion: 'Provide the target id when calling updateConditional().'
|
|
2076
|
+
});
|
|
2004
2077
|
}
|
|
2005
2078
|
|
|
2006
2079
|
const { ifMatch } = options;
|
|
2007
2080
|
if (!ifMatch) {
|
|
2008
|
-
throw new
|
|
2081
|
+
throw new ValidationError('updateConditional requires ifMatch option with ETag value', {
|
|
2082
|
+
field: 'ifMatch',
|
|
2083
|
+
statusCode: 428,
|
|
2084
|
+
retriable: false,
|
|
2085
|
+
suggestion: 'Pass the current object ETag in options.ifMatch to enable conditional updates.'
|
|
2086
|
+
});
|
|
2009
2087
|
}
|
|
2010
2088
|
|
|
2011
2089
|
// Check if resource exists
|
|
@@ -2214,7 +2292,12 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2214
2292
|
*/
|
|
2215
2293
|
async delete(id) {
|
|
2216
2294
|
if (isEmpty(id)) {
|
|
2217
|
-
throw new
|
|
2295
|
+
throw new ValidationError('Resource id cannot be empty', {
|
|
2296
|
+
field: 'id',
|
|
2297
|
+
statusCode: 400,
|
|
2298
|
+
retriable: false,
|
|
2299
|
+
suggestion: 'Provide the target id when calling delete().'
|
|
2300
|
+
});
|
|
2218
2301
|
}
|
|
2219
2302
|
|
|
2220
2303
|
let objectData;
|
|
@@ -3006,7 +3089,8 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3006
3089
|
const key = this.getResourceKey(id);
|
|
3007
3090
|
const [ok, err, response] = await tryFn(() => this.client.getObject(key));
|
|
3008
3091
|
if (!ok) {
|
|
3009
|
-
|
|
3092
|
+
// Check multiple ways the error might indicate "not found"
|
|
3093
|
+
if (err.name === "NoSuchKey" || err.code === "NoSuchKey" || err.Code === "NoSuchKey" || err.statusCode === 404) {
|
|
3010
3094
|
return {
|
|
3011
3095
|
buffer: null,
|
|
3012
3096
|
contentType: null
|
|
@@ -3801,7 +3885,15 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3801
3885
|
let idx = -1;
|
|
3802
3886
|
const stack = this._middlewares.get(method);
|
|
3803
3887
|
const dispatch = async (i) => {
|
|
3804
|
-
if (i <= idx)
|
|
3888
|
+
if (i <= idx) {
|
|
3889
|
+
throw new ResourceError('Resource middleware next() called multiple times', {
|
|
3890
|
+
resourceName: this.name,
|
|
3891
|
+
operation: method,
|
|
3892
|
+
statusCode: 500,
|
|
3893
|
+
retriable: false,
|
|
3894
|
+
suggestion: 'Ensure each middleware awaits next() at most once.'
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3805
3897
|
idx = i;
|
|
3806
3898
|
if (i < stack.length) {
|
|
3807
3899
|
return await stack[i](ctx, () => dispatch(i + 1));
|
|
@@ -3866,10 +3958,14 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3866
3958
|
|
|
3867
3959
|
const throwIfNoStateMachine = () => {
|
|
3868
3960
|
if (!resource._stateMachine) {
|
|
3869
|
-
throw new
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3961
|
+
throw new ResourceError(`State machine not configured for resource '${resource.name}'`, {
|
|
3962
|
+
resourceName: resource.name,
|
|
3963
|
+
operation: 'stateMachine',
|
|
3964
|
+
statusCode: 400,
|
|
3965
|
+
retriable: false,
|
|
3966
|
+
suggestion: 'Install and configure the StateMachinePlugin before calling resource.state.* methods.',
|
|
3967
|
+
docs: 'https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md'
|
|
3968
|
+
});
|
|
3873
3969
|
}
|
|
3874
3970
|
};
|
|
3875
3971
|
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* const users = await UserFactory.createMany(10);
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { ValidationError } from '../errors.js';
|
|
23
|
+
|
|
22
24
|
export class Factory {
|
|
23
25
|
/**
|
|
24
26
|
* Global sequence counter
|
|
@@ -161,7 +163,13 @@ export class Factory {
|
|
|
161
163
|
for (const traitName of traits) {
|
|
162
164
|
const trait = this.traits.get(traitName);
|
|
163
165
|
if (!trait) {
|
|
164
|
-
throw new
|
|
166
|
+
throw new ValidationError(`Trait '${traitName}' not found in factory '${this.resourceName}'`, {
|
|
167
|
+
field: 'trait',
|
|
168
|
+
value: traitName,
|
|
169
|
+
resourceName: this.resourceName,
|
|
170
|
+
retriable: false,
|
|
171
|
+
suggestion: `Define the trait with Factory.define('${this.resourceName}').trait('${traitName}', ...) before using it.`
|
|
172
|
+
});
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
const traitAttrs = typeof trait === 'function'
|
|
@@ -194,7 +202,11 @@ export class Factory {
|
|
|
194
202
|
const { database = Factory._database } = options;
|
|
195
203
|
|
|
196
204
|
if (!database) {
|
|
197
|
-
throw new
|
|
205
|
+
throw new ValidationError('Database not set for factory', {
|
|
206
|
+
field: 'database',
|
|
207
|
+
retriable: false,
|
|
208
|
+
suggestion: 'Call Factory.setDatabase(db) globally or pass { database } when invoking create().'
|
|
209
|
+
});
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
// Build attributes
|
|
@@ -208,7 +220,12 @@ export class Factory {
|
|
|
208
220
|
// Get resource
|
|
209
221
|
const resource = database.resources[this.resourceName];
|
|
210
222
|
if (!resource) {
|
|
211
|
-
throw new
|
|
223
|
+
throw new ValidationError(`Resource '${this.resourceName}' not found in database`, {
|
|
224
|
+
field: 'resourceName',
|
|
225
|
+
value: this.resourceName,
|
|
226
|
+
retriable: false,
|
|
227
|
+
suggestion: `Ensure the resource is created in the database before using Factory '${this.resourceName}'.`
|
|
228
|
+
});
|
|
212
229
|
}
|
|
213
230
|
|
|
214
231
|
// Create in database
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { Factory } from './factory.class.js';
|
|
19
|
+
import { ValidationError } from '../errors.js';
|
|
19
20
|
|
|
20
21
|
export class Seeder {
|
|
21
22
|
/**
|
|
@@ -59,7 +60,12 @@ export class Seeder {
|
|
|
59
60
|
|
|
60
61
|
const factory = Factory.get(resourceName);
|
|
61
62
|
if (!factory) {
|
|
62
|
-
throw new
|
|
63
|
+
throw new ValidationError(`Factory for '${resourceName}' not found`, {
|
|
64
|
+
field: 'resourceName',
|
|
65
|
+
value: resourceName,
|
|
66
|
+
retriable: false,
|
|
67
|
+
suggestion: `Register a factory with Factory.define('${resourceName}', ...) before seeding.`
|
|
68
|
+
});
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
created[resourceName] = await factory.createMany(count, {}, { database: this.database });
|