s3db.js 6.2.0 → 7.0.1

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.
Files changed (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +372 -469
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +12105 -19396
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +12090 -19393
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +12103 -19398
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -38
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +159 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,85 @@
1
+ import EventEmitter from 'events';
2
+
3
+ /**
4
+ * Base class for all replicator drivers
5
+ * Defines the interface that all replicators must implement
6
+ */
7
+ export class BaseReplicator extends EventEmitter {
8
+ constructor(config = {}) {
9
+ super();
10
+ this.config = config;
11
+ this.name = this.constructor.name;
12
+ this.enabled = config.enabled !== false; // Default to enabled unless explicitly disabled
13
+ }
14
+
15
+ /**
16
+ * Initialize the replicator
17
+ * @param {Object} database - The s3db database instance
18
+ * @returns {Promise<void>}
19
+ */
20
+ async initialize(database) {
21
+ this.database = database;
22
+ this.emit('initialized', { replicator: this.name });
23
+ }
24
+
25
+ /**
26
+ * Replicate data to the target
27
+ * @param {string} resourceName - Name of the resource being replicated
28
+ * @param {string} operation - Operation type (insert, update, delete)
29
+ * @param {Object} data - The data to replicate
30
+ * @param {string} id - Record ID
31
+ * @returns {Promise<Object>} replicator result
32
+ */
33
+ async replicate(resourceName, operation, data, id) {
34
+ throw new Error(`replicate() method must be implemented by ${this.name}`);
35
+ }
36
+
37
+ /**
38
+ * Replicate multiple records in batch
39
+ * @param {string} resourceName - Name of the resource being replicated
40
+ * @param {Array} records - Array of records to replicate
41
+ * @returns {Promise<Object>} Batch replicator result
42
+ */
43
+ async replicateBatch(resourceName, records) {
44
+ throw new Error(`replicateBatch() method must be implemented by ${this.name}`);
45
+ }
46
+
47
+ /**
48
+ * Test the connection to the target
49
+ * @returns {Promise<boolean>} True if connection is successful
50
+ */
51
+ async testConnection() {
52
+ throw new Error(`testConnection() method must be implemented by ${this.name}`);
53
+ }
54
+
55
+ /**
56
+ * Get replicator status and statistics
57
+ * @returns {Promise<Object>} Status information
58
+ */
59
+ async getStatus() {
60
+ return {
61
+ name: this.name,
62
+ // Removed: enabled: this.enabled,
63
+ config: this.config,
64
+ connected: false
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Cleanup resources
70
+ * @returns {Promise<void>}
71
+ */
72
+ async cleanup() {
73
+ this.emit('cleanup', { replicator: this.name });
74
+ }
75
+
76
+ /**
77
+ * Validate replicator configuration
78
+ * @returns {Object} Validation result
79
+ */
80
+ validateConfig() {
81
+ return { isValid: true, errors: [] };
82
+ }
83
+ }
84
+
85
+ export default BaseReplicator;
@@ -0,0 +1,328 @@
1
+ import BaseReplicator from './base-replicator.class.js';
2
+ import tryFn from "../../concerns/try-fn.js";
3
+
4
+ /**
5
+ * BigQuery Replicator
6
+ *
7
+ * Replicates data to Google BigQuery tables, supporting per-resource table mapping and action filtering.
8
+ *
9
+ * ⚠️ REQUIRED DEPENDENCY: You must install the Google Cloud BigQuery SDK to use this replicator:
10
+ *
11
+ * ```bash
12
+ * npm install @google-cloud/bigquery
13
+ * # or
14
+ * yarn add @google-cloud/bigquery
15
+ * # or
16
+ * pnpm add @google-cloud/bigquery
17
+ * ```
18
+ *
19
+ * @config {Object} config - Configuration object for the replicator
20
+ * @config {string} config.projectId - (Required) Google Cloud project ID
21
+ * @config {string} config.datasetId - (Required) BigQuery dataset ID
22
+ * @config {Object} [config.credentials] - (Optional) Google service account credentials object (JSON). If omitted, uses default credentials.
23
+ * @config {string} [config.location='US'] - (Optional) BigQuery dataset location/region
24
+ * @config {string} [config.logTable] - (Optional) Table name for operation logging. If omitted, no logging is performed.
25
+ * @config {Object} resources - Resource configuration mapping
26
+ * @config {Object|string} resources[resourceName] - Resource configuration
27
+ * @config {string} resources[resourceName].table - Table name for this resource
28
+ * @config {Array} resources[resourceName].actions - Array of actions to replicate (insert, update, delete)
29
+ * @config {string} resources[resourceName] - Short form: just the table name (equivalent to { actions: ['insert'], table: tableName })
30
+ *
31
+ * @example
32
+ * new BigqueryReplicator({
33
+ * projectId: 'my-gcp-project',
34
+ * datasetId: 'analytics',
35
+ * location: 'US',
36
+ * credentials: require('./gcp-service-account.json'),
37
+ * logTable: 's3db_replicator_log'
38
+ * }, {
39
+ * users: [
40
+ * { actions: ['insert', 'update', 'delete'], table: 'users_table' },
41
+ * ],
42
+ * urls: [
43
+ * { actions: ['insert'], table: 'urls_table' },
44
+ * { actions: ['insert'], table: 'urls_table_v2' },
45
+ * ],
46
+ * clicks: 'clicks_table' // equivalent to { actions: ['insert'], table: 'clicks_table' }
47
+ * })
48
+ *
49
+ * Notes:
50
+ * - The target tables must exist and have columns matching the resource attributes (id is required as primary key)
51
+ * - The log table must have columns: resource_name, operation, record_id, data, timestamp, source
52
+ * - Uses @google-cloud/bigquery SDK
53
+ */
54
+ class BigqueryReplicator extends BaseReplicator {
55
+ constructor(config = {}, resources = {}) {
56
+ super(config);
57
+ this.projectId = config.projectId;
58
+ this.datasetId = config.datasetId;
59
+ this.bigqueryClient = null;
60
+ this.credentials = config.credentials;
61
+ this.location = config.location || 'US';
62
+ this.logTable = config.logTable;
63
+
64
+ // Parse resources configuration
65
+ this.resources = this.parseResourcesConfig(resources);
66
+ }
67
+
68
+ parseResourcesConfig(resources) {
69
+ const parsed = {};
70
+
71
+ for (const [resourceName, config] of Object.entries(resources)) {
72
+ if (typeof config === 'string') {
73
+ // Short form: just table name
74
+ parsed[resourceName] = [{
75
+ table: config,
76
+ actions: ['insert']
77
+ }];
78
+ } else if (Array.isArray(config)) {
79
+ // Array form: multiple table mappings
80
+ parsed[resourceName] = config.map(item => {
81
+ if (typeof item === 'string') {
82
+ return { table: item, actions: ['insert'] };
83
+ }
84
+ return {
85
+ table: item.table,
86
+ actions: item.actions || ['insert']
87
+ };
88
+ });
89
+ } else if (typeof config === 'object') {
90
+ // Single object form
91
+ parsed[resourceName] = [{
92
+ table: config.table,
93
+ actions: config.actions || ['insert']
94
+ }];
95
+ }
96
+ }
97
+
98
+ return parsed;
99
+ }
100
+
101
+ validateConfig() {
102
+ const errors = [];
103
+ if (!this.projectId) errors.push('projectId is required');
104
+ if (!this.datasetId) errors.push('datasetId is required');
105
+ if (Object.keys(this.resources).length === 0) errors.push('At least one resource must be configured');
106
+
107
+ // Validate resource configurations
108
+ for (const [resourceName, tables] of Object.entries(this.resources)) {
109
+ for (const tableConfig of tables) {
110
+ if (!tableConfig.table) {
111
+ errors.push(`Table name is required for resource '${resourceName}'`);
112
+ }
113
+ if (!Array.isArray(tableConfig.actions) || tableConfig.actions.length === 0) {
114
+ errors.push(`Actions array is required for resource '${resourceName}'`);
115
+ }
116
+ const validActions = ['insert', 'update', 'delete'];
117
+ const invalidActions = tableConfig.actions.filter(action => !validActions.includes(action));
118
+ if (invalidActions.length > 0) {
119
+ errors.push(`Invalid actions for resource '${resourceName}': ${invalidActions.join(', ')}. Valid actions: ${validActions.join(', ')}`);
120
+ }
121
+ }
122
+ }
123
+
124
+ return { isValid: errors.length === 0, errors };
125
+ }
126
+
127
+ async initialize(database) {
128
+ await super.initialize(database);
129
+ const [ok, err, sdk] = await tryFn(() => import('@google-cloud/bigquery'));
130
+ if (!ok) {
131
+ this.emit('initialization_error', { replicator: this.name, error: err.message });
132
+ throw err;
133
+ }
134
+ const { BigQuery } = sdk;
135
+ this.bigqueryClient = new BigQuery({
136
+ projectId: this.projectId,
137
+ credentials: this.credentials,
138
+ location: this.location
139
+ });
140
+ this.emit('initialized', {
141
+ replicator: this.name,
142
+ projectId: this.projectId,
143
+ datasetId: this.datasetId,
144
+ resources: Object.keys(this.resources)
145
+ });
146
+ }
147
+
148
+ shouldReplicateResource(resourceName) {
149
+ return this.resources.hasOwnProperty(resourceName);
150
+ }
151
+
152
+ shouldReplicateAction(resourceName, operation) {
153
+ if (!this.resources[resourceName]) return false;
154
+
155
+ return this.resources[resourceName].some(tableConfig =>
156
+ tableConfig.actions.includes(operation)
157
+ );
158
+ }
159
+
160
+ getTablesForResource(resourceName, operation) {
161
+ if (!this.resources[resourceName]) return [];
162
+
163
+ return this.resources[resourceName]
164
+ .filter(tableConfig => tableConfig.actions.includes(operation))
165
+ .map(tableConfig => tableConfig.table);
166
+ }
167
+
168
+ async replicate(resourceName, operation, data, id, beforeData = null) {
169
+
170
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
171
+ return { skipped: true, reason: 'resource_not_included' };
172
+ }
173
+
174
+ if (!this.shouldReplicateAction(resourceName, operation)) {
175
+ return { skipped: true, reason: 'action_not_included' };
176
+ }
177
+
178
+ const tables = this.getTablesForResource(resourceName, operation);
179
+ if (tables.length === 0) {
180
+ return { skipped: true, reason: 'no_tables_for_action' };
181
+ }
182
+
183
+ const results = [];
184
+ const errors = [];
185
+
186
+ const [ok, err, result] = await tryFn(async () => {
187
+ const dataset = this.bigqueryClient.dataset(this.datasetId);
188
+ // Replicate to all applicable tables
189
+ for (const tableId of tables) {
190
+ const [okTable, errTable] = await tryFn(async () => {
191
+ const table = dataset.table(tableId);
192
+ let job;
193
+ if (operation === 'insert') {
194
+ const row = { ...data };
195
+ job = await table.insert([row]);
196
+ } else if (operation === 'update') {
197
+ const keys = Object.keys(data).filter(k => k !== 'id');
198
+ const setClause = keys.map(k => `${k}=@${k}`).join(', ');
199
+ const params = { id };
200
+ keys.forEach(k => { params[k] = data[k]; });
201
+ const query = `UPDATE \`${this.projectId}.${this.datasetId}.${tableId}\` SET ${setClause} WHERE id=@id`;
202
+ const [updateJob] = await this.bigqueryClient.createQueryJob({
203
+ query,
204
+ params
205
+ });
206
+ await updateJob.getQueryResults();
207
+ job = [updateJob];
208
+ } else if (operation === 'delete') {
209
+ const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${tableId}\` WHERE id=@id`;
210
+ const [deleteJob] = await this.bigqueryClient.createQueryJob({
211
+ query,
212
+ params: { id }
213
+ });
214
+ await deleteJob.getQueryResults();
215
+ job = [deleteJob];
216
+ } else {
217
+ throw new Error(`Unsupported operation: ${operation}`);
218
+ }
219
+ results.push({
220
+ table: tableId,
221
+ success: true,
222
+ jobId: job[0]?.id
223
+ });
224
+ });
225
+ if (!okTable) {
226
+ errors.push({
227
+ table: tableId,
228
+ error: errTable.message
229
+ });
230
+ }
231
+ }
232
+ // Log operation if logTable is configured
233
+ if (this.logTable) {
234
+ const [okLog, errLog] = await tryFn(async () => {
235
+ const logTable = dataset.table(this.logTable);
236
+ await logTable.insert([{
237
+ resource_name: resourceName,
238
+ operation,
239
+ record_id: id,
240
+ data: JSON.stringify(data),
241
+ timestamp: new Date().toISOString(),
242
+ source: 's3db-replicator'
243
+ }]);
244
+ });
245
+ if (!okLog) {
246
+ // Don't fail the main operation if logging fails
247
+ }
248
+ }
249
+ const success = errors.length === 0;
250
+ this.emit('replicated', {
251
+ replicator: this.name,
252
+ resourceName,
253
+ operation,
254
+ id,
255
+ tables,
256
+ results,
257
+ errors,
258
+ success
259
+ });
260
+ return {
261
+ success,
262
+ results,
263
+ errors,
264
+ tables
265
+ };
266
+ });
267
+ if (ok) return result;
268
+ this.emit('replicator_error', {
269
+ replicator: this.name,
270
+ resourceName,
271
+ operation,
272
+ id,
273
+ error: err.message
274
+ });
275
+ return { success: false, error: err.message };
276
+ }
277
+
278
+ async replicateBatch(resourceName, records) {
279
+ const results = [];
280
+ const errors = [];
281
+
282
+ for (const record of records) {
283
+ const [ok, err, res] = await tryFn(() => this.replicate(
284
+ resourceName,
285
+ record.operation,
286
+ record.data,
287
+ record.id,
288
+ record.beforeData
289
+ ));
290
+ if (ok) results.push(res);
291
+ else errors.push({ id: record.id, error: err.message });
292
+ }
293
+
294
+ return {
295
+ success: errors.length === 0,
296
+ results,
297
+ errors
298
+ };
299
+ }
300
+
301
+ async testConnection() {
302
+ const [ok, err] = await tryFn(async () => {
303
+ if (!this.bigqueryClient) await this.initialize();
304
+ const dataset = this.bigqueryClient.dataset(this.datasetId);
305
+ await dataset.getMetadata();
306
+ return true;
307
+ });
308
+ if (ok) return true;
309
+ this.emit('connection_error', { replicator: this.name, error: err.message });
310
+ return false;
311
+ }
312
+
313
+ async cleanup() {
314
+ // BigQuery SDK doesn't need cleanup
315
+ }
316
+
317
+ getStatus() {
318
+ return {
319
+ ...super.getStatus(),
320
+ projectId: this.projectId,
321
+ datasetId: this.datasetId,
322
+ resources: this.resources,
323
+ logTable: this.logTable
324
+ };
325
+ }
326
+ }
327
+
328
+ export default BigqueryReplicator;
@@ -0,0 +1,44 @@
1
+ import BaseReplicator from './base-replicator.class.js';
2
+ import BigqueryReplicator from './bigquery-replicator.class.js';
3
+ import PostgresReplicator from './postgres-replicator.class.js';
4
+ import S3dbReplicator from './s3db-replicator.class.js';
5
+ import SqsReplicator from './sqs-replicator.class.js';
6
+
7
+ export { BaseReplicator, BigqueryReplicator, PostgresReplicator, S3dbReplicator, SqsReplicator };
8
+
9
+ /**
10
+ * Available replicator drivers
11
+ */
12
+ export const REPLICATOR_DRIVERS = {
13
+ s3db: S3dbReplicator,
14
+ sqs: SqsReplicator,
15
+ bigquery: BigqueryReplicator,
16
+ postgres: PostgresReplicator
17
+ };
18
+
19
+ /**
20
+ * Create a replicator instance based on driver type
21
+ * @param {string} driver - Driver type (s3db, sqs, bigquery, postgres)
22
+ * @param {Object} config - Replicator configuration
23
+ * @returns {BaseReplicator} Replicator instance
24
+ */
25
+ export function createReplicator(driver, config = {}, resources = [], client = null) {
26
+ const ReplicatorClass = REPLICATOR_DRIVERS[driver];
27
+
28
+ if (!ReplicatorClass) {
29
+ throw new Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(', ')}`);
30
+ }
31
+
32
+ return new ReplicatorClass(config, resources, client);
33
+ }
34
+
35
+ /**
36
+ * Validate replicator configuration
37
+ * @param {string} driver - Driver type
38
+ * @param {Object} config - Configuration to validate
39
+ * @returns {Object} Validation result
40
+ */
41
+ export function validateReplicatorConfig(driver, config, resources = [], client = null) {
42
+ const replicator = createReplicator(driver, config, resources, client);
43
+ return replicator.validateConfig();
44
+ }