s3db.js 10.0.19 → 11.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.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Setup logic for EventualConsistencyPlugin
3
- * @module eventual-consistency/setup
2
+ * Install logic for EventualConsistencyPlugin
3
+ * @module eventual-consistency/install
4
4
  */
5
5
 
6
6
  import tryFn from "../../concerns/try-fn.js";
@@ -11,14 +11,14 @@ import { startConsolidationTimer } from "./consolidation.js";
11
11
  import { startGarbageCollectionTimer } from "./garbage-collection.js";
12
12
 
13
13
  /**
14
- * Setup plugin for all configured resources
14
+ * Install plugin for all configured resources
15
15
  *
16
16
  * @param {Object} database - Database instance
17
17
  * @param {Map} fieldHandlers - Field handlers map
18
- * @param {Function} completeFieldSetupFn - Function to complete setup for a field
18
+ * @param {Function} completeFieldSetupFn - Function to complete field setup for a field
19
19
  * @param {Function} watchForResourceFn - Function to watch for resource creation
20
20
  */
21
- export async function onSetup(database, fieldHandlers, completeFieldSetupFn, watchForResourceFn) {
21
+ export async function onInstall(database, fieldHandlers, completeFieldSetupFn, watchForResourceFn) {
22
22
  // Iterate over all resource/field combinations
23
23
  for (const [resourceName, resourceHandlers] of fieldHandlers) {
24
24
  const targetResource = database.resources[resourceName];
@@ -70,7 +70,7 @@ export function watchForResource(resourceName, database, fieldHandlers, complete
70
70
  }
71
71
 
72
72
  /**
73
- * Complete setup for a single field handler
73
+ * Complete field setup for a single field handler
74
74
  *
75
75
  * @param {Object} handler - Field handler
76
76
  * @param {Object} database - Database instance
@@ -174,6 +174,7 @@ async function createAnalyticsResource(handler, database, resourceName, fieldNam
174
174
  name: analyticsResourceName,
175
175
  attributes: {
176
176
  id: 'string|required',
177
+ field: 'string|required',
177
178
  period: 'string|required',
178
179
  cohort: 'string|required',
179
180
  transactionCount: 'number|required',
@@ -13,11 +13,10 @@ export class FullTextPlugin extends Plugin {
13
13
  this.indexes = new Map(); // In-memory index for simplicity
14
14
  }
15
15
 
16
- async setup(database) {
17
- this.database = database;
16
+ async onInstall() {
18
17
 
19
18
  // Create index resource if it doesn't exist
20
- const [ok, err, indexResource] = await tryFn(() => database.createResource({
19
+ const [ok, err, indexResource] = await tryFn(() => this.database.createResource({
21
20
  name: 'plg_fulltext_indexes',
22
21
  attributes: {
23
22
  id: 'string|required',
@@ -29,7 +28,7 @@ export class FullTextPlugin extends Plugin {
29
28
  lastUpdated: 'string|required'
30
29
  }
31
30
  }));
32
- this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
31
+ this.indexResource = ok ? indexResource : this.database.resources.fulltext_indexes;
33
32
 
34
33
  // Load existing indexes
35
34
  await this.loadIndexes();
@@ -31,12 +31,11 @@ export class MetricsPlugin extends Plugin {
31
31
  this.flushTimer = null;
32
32
  }
33
33
 
34
- async setup(database) {
35
- this.database = database;
34
+ async onInstall() {
36
35
  if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return;
37
36
 
38
37
  const [ok, err] = await tryFn(async () => {
39
- const [ok1, err1, metricsResource] = await tryFn(() => database.createResource({
38
+ const [ok1, err1, metricsResource] = await tryFn(() => this.database.createResource({
40
39
  name: 'plg_metrics',
41
40
  attributes: {
42
41
  id: 'string|required',
@@ -51,9 +50,9 @@ export class MetricsPlugin extends Plugin {
51
50
  metadata: 'json'
52
51
  }
53
52
  }));
54
- this.metricsResource = ok1 ? metricsResource : database.resources.plg_metrics;
53
+ this.metricsResource = ok1 ? metricsResource : this.database.resources.plg_metrics;
55
54
 
56
- const [ok2, err2, errorsResource] = await tryFn(() => database.createResource({
55
+ const [ok2, err2, errorsResource] = await tryFn(() => this.database.createResource({
57
56
  name: 'plg_error_logs',
58
57
  attributes: {
59
58
  id: 'string|required',
@@ -64,9 +63,9 @@ export class MetricsPlugin extends Plugin {
64
63
  metadata: 'json'
65
64
  }
66
65
  }));
67
- this.errorsResource = ok2 ? errorsResource : database.resources.plg_error_logs;
66
+ this.errorsResource = ok2 ? errorsResource : this.database.resources.plg_error_logs;
68
67
 
69
- const [ok3, err3, performanceResource] = await tryFn(() => database.createResource({
68
+ const [ok3, err3, performanceResource] = await tryFn(() => this.database.createResource({
70
69
  name: 'plg_performance_logs',
71
70
  attributes: {
72
71
  id: 'string|required',
@@ -77,13 +76,13 @@ export class MetricsPlugin extends Plugin {
77
76
  metadata: 'json'
78
77
  }
79
78
  }));
80
- this.performanceResource = ok3 ? performanceResource : database.resources.plg_performance_logs;
79
+ this.performanceResource = ok3 ? performanceResource : this.database.resources.plg_performance_logs;
81
80
  });
82
81
  if (!ok) {
83
82
  // Resources might already exist
84
- this.metricsResource = database.resources.plg_metrics;
85
- this.errorsResource = database.resources.plg_error_logs;
86
- this.performanceResource = database.resources.plg_performance_logs;
83
+ this.metricsResource = this.database.resources.plg_metrics;
84
+ this.errorsResource = this.database.resources.plg_error_logs;
85
+ this.performanceResource = this.database.resources.plg_performance_logs;
87
86
  }
88
87
 
89
88
  // Use database hooks for automatic resource discovery
@@ -1,4 +1,5 @@
1
1
  import EventEmitter from "events";
2
+ import { PluginStorage } from "../concerns/plugin-storage.js";
2
3
 
3
4
  export class Plugin extends EventEmitter {
4
5
  constructor(options = {}) {
@@ -6,13 +7,50 @@ export class Plugin extends EventEmitter {
6
7
  this.name = this.constructor.name;
7
8
  this.options = options;
8
9
  this.hooks = new Map();
10
+
11
+ // Auto-generate slug from class name (CamelCase -> kebab-case)
12
+ // e.g., EventualConsistencyPlugin -> eventual-consistency-plugin
13
+ this.slug = options.slug || this._generateSlug();
14
+
15
+ // Storage instance (lazy-loaded)
16
+ this._storage = null;
17
+ }
18
+
19
+ /**
20
+ * Generate kebab-case slug from class name
21
+ * @private
22
+ * @returns {string}
23
+ */
24
+ _generateSlug() {
25
+ return this.name
26
+ .replace(/Plugin$/, '') // Remove "Plugin" suffix
27
+ .replace(/([a-z])([A-Z])/g, '$1-$2') // CamelCase -> kebab-case
28
+ .toLowerCase();
9
29
  }
10
30
 
11
- async setup(database) {
31
+ /**
32
+ * Get PluginStorage instance (lazy-loaded)
33
+ * @returns {PluginStorage}
34
+ */
35
+ getStorage() {
36
+ if (!this._storage) {
37
+ if (!this.database || !this.database.client) {
38
+ throw new Error('Plugin must be installed before accessing storage');
39
+ }
40
+ this._storage = new PluginStorage(this.database.client, this.slug);
41
+ }
42
+ return this._storage;
43
+ }
44
+
45
+ /**
46
+ * Install plugin
47
+ * @param {Database} database - Database instance
48
+ */
49
+ async install(database) {
12
50
  this.database = database;
13
- this.beforeSetup();
14
- await this.onSetup();
15
- this.afterSetup();
51
+ this.beforeInstall();
52
+ await this.onInstall();
53
+ this.afterInstall();
16
54
  }
17
55
 
18
56
  async start() {
@@ -27,8 +65,28 @@ export class Plugin extends EventEmitter {
27
65
  this.afterStop();
28
66
  }
29
67
 
68
+ /**
69
+ * Uninstall plugin and cleanup all data
70
+ * @param {Object} options - Uninstall options
71
+ * @param {boolean} options.purgeData - Delete all plugin data from S3 (default: false)
72
+ */
73
+ async uninstall(options = {}) {
74
+ const { purgeData = false } = options;
75
+
76
+ this.beforeUninstall();
77
+ await this.onUninstall(options);
78
+
79
+ // Purge all plugin data if requested
80
+ if (purgeData && this._storage) {
81
+ const deleted = await this._storage.deleteAll();
82
+ this.emit('plugin.dataPurged', { deleted });
83
+ }
84
+
85
+ this.afterUninstall();
86
+ }
87
+
30
88
  // Override these methods in subclasses
31
- async onSetup() {
89
+ async onInstall() {
32
90
  // Override in subclasses
33
91
  }
34
92
 
@@ -40,6 +98,10 @@ export class Plugin extends EventEmitter {
40
98
  // Override in subclasses
41
99
  }
42
100
 
101
+ async onUninstall(options) {
102
+ // Override in subclasses
103
+ }
104
+
43
105
  // Hook management methods
44
106
  addHook(resource, event, handler) {
45
107
  if (!this.hooks.has(resource)) {
@@ -182,12 +244,12 @@ export class Plugin extends EventEmitter {
182
244
  }
183
245
 
184
246
  // Event emission methods
185
- beforeSetup() {
186
- this.emit("plugin.beforeSetup", new Date());
247
+ beforeInstall() {
248
+ this.emit("plugin.beforeInstall", new Date());
187
249
  }
188
250
 
189
- afterSetup() {
190
- this.emit("plugin.afterSetup", new Date());
251
+ afterInstall() {
252
+ this.emit("plugin.afterInstall", new Date());
191
253
  }
192
254
 
193
255
  beforeStart() {
@@ -205,6 +267,14 @@ export class Plugin extends EventEmitter {
205
267
  afterStop() {
206
268
  this.emit("plugin.afterStop", new Date());
207
269
  }
270
+
271
+ beforeUninstall() {
272
+ this.emit("plugin.beforeUninstall", new Date());
273
+ }
274
+
275
+ afterUninstall() {
276
+ this.emit("plugin.afterUninstall", new Date());
277
+ }
208
278
  }
209
279
 
210
280
  export default Plugin;
@@ -1,3 +1,4 @@
1
+ import { Plugin } from './plugin.class.js';
1
2
  import { createConsumer } from './consumers/index.js';
2
3
  import tryFn from "../concerns/try-fn.js";
3
4
 
@@ -20,16 +21,16 @@ import tryFn from "../concerns/try-fn.js";
20
21
  // reconnectInterval: 2000,
21
22
  // });
22
23
 
23
- export class QueueConsumerPlugin {
24
+ export class QueueConsumerPlugin extends Plugin {
24
25
  constructor(options = {}) {
26
+ super(options);
25
27
  this.options = options;
26
28
  // New pattern: consumers = [{ driver, config, consumers: [{ queueUrl, resources, ... }] }]
27
29
  this.driversConfig = Array.isArray(options.consumers) ? options.consumers : [];
28
30
  this.consumers = [];
29
31
  }
30
32
 
31
- async setup(database) {
32
- this.database = database;
33
+ async onInstall() {
33
34
 
34
35
  for (const driverDef of this.driversConfig) {
35
36
  const { driver, config: driverConfig = {}, consumers: consumerDefs = [] } = driverDef;
@@ -236,12 +236,10 @@ export class ReplicatorPlugin extends Plugin {
236
236
  this.eventListenersInstalled.add(resource.name);
237
237
  }
238
238
 
239
- async setup(database) {
240
- this.database = database;
241
-
239
+ async onInstall() {
242
240
  // Create replicator log resource if enabled
243
241
  if (this.config.persistReplicatorLog) {
244
- const [ok, err, logResource] = await tryFn(() => database.createResource({
242
+ const [ok, err, logResource] = await tryFn(() => this.database.createResource({
245
243
  name: this.config.replicatorLogResource || 'plg_replicator_logs',
246
244
  attributes: {
247
245
  id: 'string|required',
@@ -253,24 +251,24 @@ export class ReplicatorPlugin extends Plugin {
253
251
  },
254
252
  behavior: 'truncate-data'
255
253
  }));
256
-
254
+
257
255
  if (ok) {
258
256
  this.replicatorLogResource = logResource;
259
257
  } else {
260
- this.replicatorLogResource = database.resources[this.config.replicatorLogResource || 'plg_replicator_logs'];
258
+ this.replicatorLogResource = this.database.resources[this.config.replicatorLogResource || 'plg_replicator_logs'];
261
259
  }
262
260
  }
263
261
 
264
262
  // Initialize replicators
265
- await this.initializeReplicators(database);
266
-
263
+ await this.initializeReplicators(this.database);
264
+
267
265
  // Use database hooks for automatic resource discovery
268
266
  this.installDatabaseHooks();
269
-
267
+
270
268
  // Install event listeners for existing resources
271
- for (const resource of Object.values(database.resources)) {
269
+ for (const resource of Object.values(this.database.resources)) {
272
270
  if (resource.name !== (this.config.replicatorLogResource || 'plg_replicator_logs')) {
273
- this.installEventListeners(resource, database, this);
271
+ this.installEventListeners(resource, this.database, this);
274
272
  }
275
273
  }
276
274
  }
@@ -334,8 +332,8 @@ export class ReplicatorPlugin extends Plugin {
334
332
  }
335
333
 
336
334
  async uploadMetadataFile(database) {
337
- if (typeof database.uploadMetadataFile === 'function') {
338
- await database.uploadMetadataFile();
335
+ if (typeof this.database.uploadMetadataFile === 'function') {
336
+ await this.database.uploadMetadataFile();
339
337
  }
340
338
  }
341
339
 
@@ -97,7 +97,7 @@ export class S3QueuePlugin extends Plugin {
97
97
  this.lockCleanupInterval = null;
98
98
  }
99
99
 
100
- async onSetup() {
100
+ async onInstall() {
101
101
  // Get target resource
102
102
  this.targetResource = this.database.resources[this.config.resource];
103
103
  if (!this.targetResource) {
@@ -29,11 +29,11 @@ import { idGenerator } from "../concerns/id.js";
29
29
  * schedule: '0 3 * * *',
30
30
  * description: 'Clean up expired records',
31
31
  * action: async (database, context) => {
32
- * const expired = await database.resource('sessions')
32
+ * const expired = await this.database.resource('sessions')
33
33
  * .list({ where: { expiresAt: { $lt: new Date() } } });
34
34
  *
35
35
  * for (const record of expired) {
36
- * await database.resource('sessions').delete(record.id);
36
+ * await this.database.resource('sessions').delete(record.id);
37
37
  * }
38
38
  *
39
39
  * return { deleted: expired.length };
@@ -48,8 +48,8 @@ import { idGenerator } from "../concerns/id.js";
48
48
  * schedule: '0 9 * * MON',
49
49
  * description: 'Generate weekly analytics report',
50
50
  * action: async (database, context) => {
51
- * const users = await database.resource('users').count();
52
- * const orders = await database.resource('orders').count({
51
+ * const users = await this.database.resource('users').count();
52
+ * const orders = await this.database.resource('orders').count({
53
53
  * where: {
54
54
  * createdAt: {
55
55
  * $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
@@ -64,7 +64,7 @@ import { idGenerator } from "../concerns/id.js";
64
64
  * createdAt: new Date().toISOString()
65
65
  * };
66
66
  *
67
- * await database.resource('reports').insert(report);
67
+ * await this.database.resource('reports').insert(report);
68
68
  * return report;
69
69
  * }
70
70
  * },
@@ -106,7 +106,7 @@ import { idGenerator } from "../concerns/id.js";
106
106
  * const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
107
107
  *
108
108
  * // Aggregate metrics from the last hour
109
- * const events = await database.resource('events').list({
109
+ * const events = await this.database.resource('events').list({
110
110
  * where: {
111
111
  * timestamp: {
112
112
  * $gte: hourAgo.getTime(),
@@ -120,7 +120,7 @@ import { idGenerator } from "../concerns/id.js";
120
120
  * return acc;
121
121
  * }, {});
122
122
  *
123
- * await database.resource('hourly_metrics').insert({
123
+ * await this.database.resource('hourly_metrics').insert({
124
124
  * hour: hourAgo.toISOString().slice(0, 13),
125
125
  * metrics: aggregated,
126
126
  * total: events.length,
@@ -217,8 +217,7 @@ export class SchedulerPlugin extends Plugin {
217
217
  return true; // Simplified validation
218
218
  }
219
219
 
220
- async setup(database) {
221
- this.database = database;
220
+ async onInstall() {
222
221
 
223
222
  // Create lock resource for distributed locking
224
223
  await this._createLockResource();
@@ -61,7 +61,7 @@ import tryFn from "../concerns/try-fn.js";
61
61
  *
62
62
  * actions: {
63
63
  * onConfirmed: async (context, event, machine) => {
64
- * await machine.database.resource('inventory').update(context.productId, {
64
+ * await machine.this.database.resource('inventory').update(context.productId, {
65
65
  * quantity: { $decrement: context.quantity }
66
66
  * });
67
67
  * await machine.sendNotification(context.customerEmail, 'order_confirmed');
@@ -73,7 +73,7 @@ import tryFn from "../concerns/try-fn.js";
73
73
  *
74
74
  * guards: {
75
75
  * canShip: async (context, event, machine) => {
76
- * const inventory = await machine.database.resource('inventory').get(context.productId);
76
+ * const inventory = await machine.this.database.resource('inventory').get(context.productId);
77
77
  * return inventory.quantity >= context.quantity;
78
78
  * }
79
79
  * },
@@ -138,8 +138,7 @@ export class StateMachinePlugin extends Plugin {
138
138
  }
139
139
  }
140
140
 
141
- async setup(database) {
142
- this.database = database;
141
+ async onInstall() {
143
142
 
144
143
  // Create state storage resource if persistence is enabled
145
144
  if (this.config.persistTransitions) {