s3db.js 6.2.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGINS.md +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +30057 -18387
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +30043 -18384
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +29730 -18061
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -69
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +142 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import EventEmitter from "events";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { isEmpty, isFunction } from "lodash-es";
|
|
4
|
+
import jsonStableStringify from "json-stable-stringify";
|
|
5
|
+
|
|
6
|
+
import Client from "./client.class.js";
|
|
7
|
+
import tryFn from "./concerns/try-fn.js";
|
|
8
|
+
import Resource from "./resource.class.js";
|
|
9
|
+
import { streamToString } from "./stream/index.js";
|
|
10
|
+
import { ResourceNotFound } from "./errors.js";
|
|
11
|
+
|
|
12
|
+
export class Database extends EventEmitter {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super();
|
|
15
|
+
|
|
16
|
+
this.version = "1";
|
|
17
|
+
// Version is injected during build, fallback to "latest" for development
|
|
18
|
+
this.s3dbVersion = (() => {
|
|
19
|
+
const [ok, err, version] = tryFn(() => (typeof __PACKAGE_VERSION__ !== 'undefined' && __PACKAGE_VERSION__ !== '__PACKAGE_VERSION__'
|
|
20
|
+
? __PACKAGE_VERSION__
|
|
21
|
+
: "latest"));
|
|
22
|
+
return ok ? version : "latest";
|
|
23
|
+
})();
|
|
24
|
+
this.resources = {};
|
|
25
|
+
this.savedMetadata = null; // Store loaded metadata for versioning
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
this.parallelism = parseInt(options.parallelism + "") || 10;
|
|
29
|
+
this.plugins = options.plugins || []; // Initialize plugins array
|
|
30
|
+
this.pluginList = options.plugins || []; // Keep the list for backward compatibility
|
|
31
|
+
this.cache = options.cache;
|
|
32
|
+
this.passphrase = options.passphrase || "secret";
|
|
33
|
+
this.versioningEnabled = options.versioningEnabled || false;
|
|
34
|
+
|
|
35
|
+
// Handle both connection string and individual parameters
|
|
36
|
+
let connectionString = options.connectionString;
|
|
37
|
+
if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
|
|
38
|
+
// Build connection string manually
|
|
39
|
+
const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
|
|
40
|
+
|
|
41
|
+
// If endpoint is provided, assume it's MinIO or Digital Ocean
|
|
42
|
+
if (endpoint) {
|
|
43
|
+
const url = new URL(endpoint);
|
|
44
|
+
if (accessKeyId) url.username = encodeURIComponent(accessKeyId);
|
|
45
|
+
if (secretAccessKey) url.password = encodeURIComponent(secretAccessKey);
|
|
46
|
+
url.pathname = `/${bucket || 's3db'}`;
|
|
47
|
+
|
|
48
|
+
// Add forcePathStyle parameter if specified
|
|
49
|
+
if (forcePathStyle) {
|
|
50
|
+
url.searchParams.set('forcePathStyle', 'true');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
connectionString = url.toString();
|
|
54
|
+
} else if (accessKeyId && secretAccessKey) {
|
|
55
|
+
// Otherwise, build S3 connection string only if credentials are provided
|
|
56
|
+
const params = new URLSearchParams();
|
|
57
|
+
params.set('region', region || 'us-east-1');
|
|
58
|
+
if (forcePathStyle) {
|
|
59
|
+
params.set('forcePathStyle', 'true');
|
|
60
|
+
}
|
|
61
|
+
connectionString = `s3://${encodeURIComponent(accessKeyId)}:${encodeURIComponent(secretAccessKey)}@${bucket || 's3db'}?${params.toString()}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.client = options.client || new Client({
|
|
66
|
+
verbose: this.verbose,
|
|
67
|
+
parallelism: this.parallelism,
|
|
68
|
+
connectionString: connectionString,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.bucket = this.client.bucket;
|
|
72
|
+
this.keyPrefix = this.client.keyPrefix;
|
|
73
|
+
|
|
74
|
+
// Add process exit listener for cleanup
|
|
75
|
+
if (!this._exitListenerRegistered) {
|
|
76
|
+
this._exitListenerRegistered = true;
|
|
77
|
+
process.on('exit', async () => {
|
|
78
|
+
if (this.isConnected()) {
|
|
79
|
+
try {
|
|
80
|
+
await this.disconnect();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// Silently ignore errors on exit
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async connect() {
|
|
90
|
+
await this.startPlugins();
|
|
91
|
+
|
|
92
|
+
let metadata = null;
|
|
93
|
+
|
|
94
|
+
if (await this.client.exists(`s3db.json`)) {
|
|
95
|
+
const request = await this.client.getObject(`s3db.json`);
|
|
96
|
+
metadata = JSON.parse(await streamToString(request?.Body));
|
|
97
|
+
} else {
|
|
98
|
+
metadata = this.blankMetadataStructure();
|
|
99
|
+
await this.uploadMetadataFile();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.savedMetadata = metadata;
|
|
103
|
+
|
|
104
|
+
// Check for definition changes (this happens before creating resources from createResource calls)
|
|
105
|
+
const definitionChanges = this.detectDefinitionChanges(metadata);
|
|
106
|
+
|
|
107
|
+
// Create resources from saved metadata using current version
|
|
108
|
+
for (const [name, resourceMetadata] of Object.entries(metadata.resources || {})) {
|
|
109
|
+
const currentVersion = resourceMetadata.currentVersion || 'v0';
|
|
110
|
+
const versionData = resourceMetadata.versions?.[currentVersion];
|
|
111
|
+
|
|
112
|
+
if (versionData) {
|
|
113
|
+
// Extract configuration from version data at root level
|
|
114
|
+
this.resources[name] = new Resource({
|
|
115
|
+
name,
|
|
116
|
+
client: this.client,
|
|
117
|
+
database: this, // garantir referência
|
|
118
|
+
version: currentVersion,
|
|
119
|
+
attributes: versionData.attributes,
|
|
120
|
+
behavior: versionData.behavior || 'user-managed',
|
|
121
|
+
parallelism: this.parallelism,
|
|
122
|
+
passphrase: this.passphrase,
|
|
123
|
+
observers: [this],
|
|
124
|
+
cache: this.cache,
|
|
125
|
+
timestamps: versionData.timestamps !== undefined ? versionData.timestamps : false,
|
|
126
|
+
partitions: resourceMetadata.partitions || versionData.partitions || {},
|
|
127
|
+
paranoid: versionData.paranoid !== undefined ? versionData.paranoid : true,
|
|
128
|
+
allNestedObjectsOptional: versionData.allNestedObjectsOptional !== undefined ? versionData.allNestedObjectsOptional : true,
|
|
129
|
+
autoDecrypt: versionData.autoDecrypt !== undefined ? versionData.autoDecrypt : true,
|
|
130
|
+
hooks: versionData.hooks || {},
|
|
131
|
+
versioningEnabled: this.versioningEnabled,
|
|
132
|
+
map: versionData.map
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Emit definition changes if any were detected
|
|
138
|
+
if (definitionChanges.length > 0) {
|
|
139
|
+
this.emit("resourceDefinitionsChanged", {
|
|
140
|
+
changes: definitionChanges,
|
|
141
|
+
metadata: this.savedMetadata
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.emit("connected", new Date());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detect changes in resource definitions compared to saved metadata
|
|
150
|
+
* @param {Object} savedMetadata - The metadata loaded from s3db.json
|
|
151
|
+
* @returns {Array} Array of change objects
|
|
152
|
+
*/
|
|
153
|
+
detectDefinitionChanges(savedMetadata) {
|
|
154
|
+
const changes = [];
|
|
155
|
+
|
|
156
|
+
for (const [name, currentResource] of Object.entries(this.resources)) {
|
|
157
|
+
const currentHash = this.generateDefinitionHash(currentResource.export());
|
|
158
|
+
const savedResource = savedMetadata.resources?.[name];
|
|
159
|
+
|
|
160
|
+
if (!savedResource) {
|
|
161
|
+
changes.push({
|
|
162
|
+
type: 'new',
|
|
163
|
+
resourceName: name,
|
|
164
|
+
currentHash,
|
|
165
|
+
savedHash: null
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
// Get current version hash from saved metadata
|
|
169
|
+
const currentVersion = savedResource.currentVersion || 'v0';
|
|
170
|
+
const versionData = savedResource.versions?.[currentVersion];
|
|
171
|
+
const savedHash = versionData?.hash;
|
|
172
|
+
|
|
173
|
+
if (savedHash !== currentHash) {
|
|
174
|
+
changes.push({
|
|
175
|
+
type: 'changed',
|
|
176
|
+
resourceName: name,
|
|
177
|
+
currentHash,
|
|
178
|
+
savedHash,
|
|
179
|
+
fromVersion: currentVersion,
|
|
180
|
+
toVersion: this.getNextVersion(savedResource.versions)
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for deleted resources
|
|
187
|
+
for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
|
|
188
|
+
if (!this.resources[name]) {
|
|
189
|
+
const currentVersion = savedResource.currentVersion || 'v0';
|
|
190
|
+
const versionData = savedResource.versions?.[currentVersion];
|
|
191
|
+
changes.push({
|
|
192
|
+
type: 'deleted',
|
|
193
|
+
resourceName: name,
|
|
194
|
+
currentHash: null,
|
|
195
|
+
savedHash: versionData?.hash,
|
|
196
|
+
deletedVersion: currentVersion
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return changes;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generate a consistent hash for a resource definition
|
|
206
|
+
* @param {Object} definition - Resource definition to hash
|
|
207
|
+
* @param {string} behavior - Resource behavior
|
|
208
|
+
* @returns {string} SHA256 hash
|
|
209
|
+
*/
|
|
210
|
+
generateDefinitionHash(definition, behavior = undefined) {
|
|
211
|
+
// Extract only the attributes for hashing (exclude name, version, options, etc.)
|
|
212
|
+
const attributes = definition.attributes;
|
|
213
|
+
// Create a stable version for hashing by excluding dynamic fields
|
|
214
|
+
const stableAttributes = { ...attributes };
|
|
215
|
+
// Remove timestamp fields if they were added automatically
|
|
216
|
+
if (definition.timestamps) {
|
|
217
|
+
delete stableAttributes.createdAt;
|
|
218
|
+
delete stableAttributes.updatedAt;
|
|
219
|
+
}
|
|
220
|
+
// Include behavior and partitions in the hash
|
|
221
|
+
const hashObj = {
|
|
222
|
+
attributes: stableAttributes,
|
|
223
|
+
behavior: behavior || definition.behavior || 'user-managed',
|
|
224
|
+
partitions: definition.partitions || {},
|
|
225
|
+
};
|
|
226
|
+
// Use jsonStableStringify to ensure consistent ordering
|
|
227
|
+
const stableString = jsonStableStringify(hashObj);
|
|
228
|
+
return `sha256:${createHash('sha256').update(stableString).digest('hex')}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the next version number for a resource
|
|
233
|
+
* @param {Object} versions - Existing versions object
|
|
234
|
+
* @returns {string} Next version string (e.g., 'v1', 'v2')
|
|
235
|
+
*/
|
|
236
|
+
getNextVersion(versions = {}) {
|
|
237
|
+
const versionNumbers = Object.keys(versions)
|
|
238
|
+
.filter(v => v.startsWith('v'))
|
|
239
|
+
.map(v => parseInt(v.substring(1)))
|
|
240
|
+
.filter(n => !isNaN(n));
|
|
241
|
+
|
|
242
|
+
const maxVersion = versionNumbers.length > 0 ? Math.max(...versionNumbers) : -1;
|
|
243
|
+
return `v${maxVersion + 1}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async startPlugins() {
|
|
247
|
+
const db = this
|
|
248
|
+
|
|
249
|
+
if (!isEmpty(this.pluginList)) {
|
|
250
|
+
const plugins = this.pluginList.map(p => isFunction(p) ? new p(this) : p)
|
|
251
|
+
|
|
252
|
+
const setupProms = plugins.map(async (plugin) => {
|
|
253
|
+
if (plugin.beforeSetup) await plugin.beforeSetup()
|
|
254
|
+
await plugin.setup(db)
|
|
255
|
+
if (plugin.afterSetup) await plugin.afterSetup()
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await Promise.all(setupProms);
|
|
259
|
+
|
|
260
|
+
const startProms = plugins.map(async (plugin) => {
|
|
261
|
+
if (plugin.beforeStart) await plugin.beforeStart()
|
|
262
|
+
await plugin.start()
|
|
263
|
+
if (plugin.afterStart) await plugin.afterStart()
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await Promise.all(startProms);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Register and setup a plugin
|
|
272
|
+
* @param {Plugin} plugin - Plugin instance to register
|
|
273
|
+
* @param {string} [name] - Optional name for the plugin (defaults to plugin.constructor.name)
|
|
274
|
+
*/
|
|
275
|
+
async usePlugin(plugin, name = null) {
|
|
276
|
+
const pluginName = name || plugin.constructor.name.replace('Plugin', '').toLowerCase();
|
|
277
|
+
|
|
278
|
+
// Register the plugin
|
|
279
|
+
this.plugins[pluginName] = plugin;
|
|
280
|
+
|
|
281
|
+
// Setup the plugin if database is connected
|
|
282
|
+
if (this.isConnected()) {
|
|
283
|
+
await plugin.setup(this);
|
|
284
|
+
await plugin.start();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return plugin;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async uploadMetadataFile() {
|
|
291
|
+
const metadata = {
|
|
292
|
+
version: this.version,
|
|
293
|
+
s3dbVersion: this.s3dbVersion,
|
|
294
|
+
lastUpdated: new Date().toISOString(),
|
|
295
|
+
resources: {}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Generate versioned definition for each resource
|
|
299
|
+
Object.entries(this.resources).forEach(([name, resource]) => {
|
|
300
|
+
const resourceDef = resource.export();
|
|
301
|
+
const definitionHash = this.generateDefinitionHash(resourceDef);
|
|
302
|
+
|
|
303
|
+
// Check if resource exists in saved metadata
|
|
304
|
+
const existingResource = this.savedMetadata?.resources?.[name];
|
|
305
|
+
const currentVersion = existingResource?.currentVersion || 'v0';
|
|
306
|
+
const existingVersionData = existingResource?.versions?.[currentVersion];
|
|
307
|
+
|
|
308
|
+
let version, isNewVersion;
|
|
309
|
+
|
|
310
|
+
// If hash is different, create new version
|
|
311
|
+
if (!existingVersionData || existingVersionData.hash !== definitionHash) {
|
|
312
|
+
version = this.getNextVersion(existingResource?.versions);
|
|
313
|
+
isNewVersion = true;
|
|
314
|
+
} else {
|
|
315
|
+
version = currentVersion;
|
|
316
|
+
isNewVersion = false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
metadata.resources[name] = {
|
|
320
|
+
currentVersion: version,
|
|
321
|
+
partitions: resource.config.partitions || {},
|
|
322
|
+
versions: {
|
|
323
|
+
...existingResource?.versions, // Preserve previous versions
|
|
324
|
+
[version]: {
|
|
325
|
+
hash: definitionHash,
|
|
326
|
+
attributes: resourceDef.attributes,
|
|
327
|
+
behavior: resourceDef.behavior || 'user-managed',
|
|
328
|
+
timestamps: resource.config.timestamps,
|
|
329
|
+
partitions: resource.config.partitions,
|
|
330
|
+
paranoid: resource.config.paranoid,
|
|
331
|
+
allNestedObjectsOptional: resource.config.allNestedObjectsOptional,
|
|
332
|
+
autoDecrypt: resource.config.autoDecrypt,
|
|
333
|
+
cache: resource.config.cache,
|
|
334
|
+
hooks: resource.config.hooks,
|
|
335
|
+
createdAt: isNewVersion ? new Date().toISOString() : existingVersionData?.createdAt
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Update resource version safely
|
|
341
|
+
if (resource.version !== version) {
|
|
342
|
+
resource.version = version;
|
|
343
|
+
resource.emit('versionUpdated', { oldVersion: currentVersion, newVersion: version });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await this.client.putObject({
|
|
348
|
+
key: 's3db.json',
|
|
349
|
+
body: JSON.stringify(metadata, null, 2),
|
|
350
|
+
contentType: 'application/json'
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
this.savedMetadata = metadata;
|
|
354
|
+
this.emit('metadataUploaded', metadata);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
blankMetadataStructure() {
|
|
358
|
+
return {
|
|
359
|
+
version: `1`,
|
|
360
|
+
s3dbVersion: this.s3dbVersion,
|
|
361
|
+
resources: {},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if a resource exists by name
|
|
367
|
+
* @param {string} name - Resource name
|
|
368
|
+
* @returns {boolean} True if resource exists, false otherwise
|
|
369
|
+
*/
|
|
370
|
+
resourceExists(name) {
|
|
371
|
+
return !!this.resources[name];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if a resource exists with the same definition hash
|
|
376
|
+
* @param {Object} config - Resource configuration
|
|
377
|
+
* @param {string} config.name - Resource name
|
|
378
|
+
* @param {Object} config.attributes - Resource attributes
|
|
379
|
+
* @param {string} [config.behavior] - Resource behavior
|
|
380
|
+
* @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
|
|
381
|
+
* @returns {Object} Result with exists and hash information
|
|
382
|
+
*/
|
|
383
|
+
resourceExistsWithSameHash({ name, attributes, behavior = 'user-managed', partitions = {}, options = {} }) {
|
|
384
|
+
if (!this.resources[name]) {
|
|
385
|
+
return { exists: false, sameHash: false, hash: null };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const existingResource = this.resources[name];
|
|
389
|
+
const existingHash = this.generateDefinitionHash(existingResource.export());
|
|
390
|
+
|
|
391
|
+
// Create a mock resource to calculate the new hash
|
|
392
|
+
const mockResource = new Resource({
|
|
393
|
+
name,
|
|
394
|
+
attributes,
|
|
395
|
+
behavior,
|
|
396
|
+
partitions,
|
|
397
|
+
client: this.client,
|
|
398
|
+
version: existingResource.version,
|
|
399
|
+
passphrase: this.passphrase,
|
|
400
|
+
versioningEnabled: this.versioningEnabled,
|
|
401
|
+
...options
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const newHash = this.generateDefinitionHash(mockResource.export());
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
exists: true,
|
|
408
|
+
sameHash: existingHash === newHash,
|
|
409
|
+
hash: newHash,
|
|
410
|
+
existingHash
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async createResource({ name, attributes, behavior = 'user-managed', hooks, ...config }) {
|
|
415
|
+
if (this.resources[name]) {
|
|
416
|
+
const existingResource = this.resources[name];
|
|
417
|
+
// Update configuration
|
|
418
|
+
Object.assign(existingResource.config, {
|
|
419
|
+
cache: this.cache,
|
|
420
|
+
...config,
|
|
421
|
+
});
|
|
422
|
+
if (behavior) {
|
|
423
|
+
existingResource.behavior = behavior;
|
|
424
|
+
}
|
|
425
|
+
// Ensure versioning configuration is set
|
|
426
|
+
existingResource.versioningEnabled = this.versioningEnabled;
|
|
427
|
+
existingResource.updateAttributes(attributes);
|
|
428
|
+
// NOVO: Mescla hooks se fornecidos (append ao final)
|
|
429
|
+
if (hooks) {
|
|
430
|
+
for (const [event, hooksArr] of Object.entries(hooks)) {
|
|
431
|
+
if (Array.isArray(hooksArr) && existingResource.hooks[event]) {
|
|
432
|
+
for (const fn of hooksArr) {
|
|
433
|
+
if (typeof fn === 'function') {
|
|
434
|
+
existingResource.hooks[event].push(fn.bind(existingResource));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Only upload metadata if hash actually changed
|
|
441
|
+
const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
|
|
442
|
+
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
443
|
+
const currentVersion = existingMetadata?.currentVersion || 'v0';
|
|
444
|
+
const existingVersionData = existingMetadata?.versions?.[currentVersion];
|
|
445
|
+
if (!existingVersionData || existingVersionData.hash !== newHash) {
|
|
446
|
+
await this.uploadMetadataFile();
|
|
447
|
+
}
|
|
448
|
+
this.emit("s3db.resourceUpdated", name);
|
|
449
|
+
return existingResource;
|
|
450
|
+
}
|
|
451
|
+
const existingMetadata = this.savedMetadata?.resources?.[name];
|
|
452
|
+
const version = existingMetadata?.currentVersion || 'v0';
|
|
453
|
+
const resource = new Resource({
|
|
454
|
+
name,
|
|
455
|
+
client: this.client,
|
|
456
|
+
version: config.version !== undefined ? config.version : version,
|
|
457
|
+
attributes,
|
|
458
|
+
behavior,
|
|
459
|
+
parallelism: this.parallelism,
|
|
460
|
+
passphrase: config.passphrase !== undefined ? config.passphrase : this.passphrase,
|
|
461
|
+
observers: [this],
|
|
462
|
+
cache: config.cache !== undefined ? config.cache : this.cache,
|
|
463
|
+
timestamps: config.timestamps !== undefined ? config.timestamps : false,
|
|
464
|
+
partitions: config.partitions || {},
|
|
465
|
+
paranoid: config.paranoid !== undefined ? config.paranoid : true,
|
|
466
|
+
allNestedObjectsOptional: config.allNestedObjectsOptional !== undefined ? config.allNestedObjectsOptional : true,
|
|
467
|
+
autoDecrypt: config.autoDecrypt !== undefined ? config.autoDecrypt : true,
|
|
468
|
+
hooks: hooks || {},
|
|
469
|
+
versioningEnabled: this.versioningEnabled,
|
|
470
|
+
map: config.map,
|
|
471
|
+
idGenerator: config.idGenerator,
|
|
472
|
+
idSize: config.idSize
|
|
473
|
+
});
|
|
474
|
+
resource.database = this;
|
|
475
|
+
this.resources[name] = resource;
|
|
476
|
+
await this.uploadMetadataFile();
|
|
477
|
+
this.emit("s3db.resourceCreated", name);
|
|
478
|
+
return resource;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
resource(name) {
|
|
482
|
+
if (!this.resources[name]) {
|
|
483
|
+
return Promise.reject(`resource ${name} does not exist`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return this.resources[name];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* List all resource names
|
|
491
|
+
* @returns {Array} Array of resource names
|
|
492
|
+
*/
|
|
493
|
+
async listResources() {
|
|
494
|
+
return Object.keys(this.resources).map(name => ({ name }));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get a specific resource by name
|
|
499
|
+
* @param {string} name - Resource name
|
|
500
|
+
* @returns {Resource} Resource instance
|
|
501
|
+
*/
|
|
502
|
+
async getResource(name) {
|
|
503
|
+
if (!this.resources[name]) {
|
|
504
|
+
throw new ResourceNotFound({
|
|
505
|
+
bucket: this.client.config.bucket,
|
|
506
|
+
resourceName: name,
|
|
507
|
+
id: name
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return this.resources[name];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get database configuration
|
|
515
|
+
* @returns {Object} Configuration object
|
|
516
|
+
*/
|
|
517
|
+
get config() {
|
|
518
|
+
return {
|
|
519
|
+
version: this.version,
|
|
520
|
+
s3dbVersion: this.s3dbVersion,
|
|
521
|
+
bucket: this.bucket,
|
|
522
|
+
keyPrefix: this.keyPrefix,
|
|
523
|
+
parallelism: this.parallelism,
|
|
524
|
+
verbose: this.verbose
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
isConnected() {
|
|
529
|
+
return !!this.savedMetadata;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async disconnect() {
|
|
533
|
+
try {
|
|
534
|
+
// 1. Remove all listeners from all plugins
|
|
535
|
+
if (this.pluginList && this.pluginList.length > 0) {
|
|
536
|
+
for (const plugin of this.pluginList) {
|
|
537
|
+
if (plugin && typeof plugin.removeAllListeners === 'function') {
|
|
538
|
+
plugin.removeAllListeners();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Also stop plugins if they have a stop method
|
|
542
|
+
const stopProms = this.pluginList.map(async (plugin) => {
|
|
543
|
+
try {
|
|
544
|
+
if (plugin && typeof plugin.stop === 'function') {
|
|
545
|
+
await plugin.stop();
|
|
546
|
+
}
|
|
547
|
+
} catch (err) {
|
|
548
|
+
// Silently ignore errors on exit
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
await Promise.all(stopProms);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 2. Remove all listeners from all resources
|
|
555
|
+
if (this.resources && Object.keys(this.resources).length > 0) {
|
|
556
|
+
for (const [name, resource] of Object.entries(this.resources)) {
|
|
557
|
+
try {
|
|
558
|
+
if (resource && typeof resource.removeAllListeners === 'function') {
|
|
559
|
+
resource.removeAllListeners();
|
|
560
|
+
}
|
|
561
|
+
if (resource._pluginWrappers) {
|
|
562
|
+
resource._pluginWrappers.clear();
|
|
563
|
+
}
|
|
564
|
+
if (resource._pluginMiddlewares) {
|
|
565
|
+
resource._pluginMiddlewares = {};
|
|
566
|
+
}
|
|
567
|
+
if (resource.observers && Array.isArray(resource.observers)) {
|
|
568
|
+
resource.observers = [];
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
// Silently ignore errors on exit
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Instead of reassigning, clear in place
|
|
575
|
+
Object.keys(this.resources).forEach(k => delete this.resources[k]);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// 3. Remove all listeners from the client
|
|
579
|
+
if (this.client && typeof this.client.removeAllListeners === 'function') {
|
|
580
|
+
this.client.removeAllListeners();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 4. Remove all listeners from the database itself
|
|
584
|
+
this.removeAllListeners();
|
|
585
|
+
|
|
586
|
+
// 5. Clear saved metadata and plugin lists
|
|
587
|
+
this.savedMetadata = null;
|
|
588
|
+
this.plugins = {};
|
|
589
|
+
this.pluginList = [];
|
|
590
|
+
|
|
591
|
+
this.emit('disconnected', new Date());
|
|
592
|
+
} catch (err) {
|
|
593
|
+
// Silently ignore errors on exit
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export class S3db extends Database {}
|
|
599
|
+
export default S3db;
|