s3db.js 6.1.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.
Files changed (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +377 -492
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +30054 -18189
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +30040 -18186
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +29727 -17863
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -69
  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 +142 -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,210 @@
1
+ import EventEmitter from "events";
2
+
3
+ export class Plugin extends EventEmitter {
4
+ constructor(options = {}) {
5
+ super();
6
+ this.name = this.constructor.name;
7
+ this.options = options;
8
+ this.hooks = new Map();
9
+ }
10
+
11
+ async setup(database) {
12
+ this.database = database;
13
+ this.beforeSetup();
14
+ await this.onSetup();
15
+ this.afterSetup();
16
+ }
17
+
18
+ async start() {
19
+ this.beforeStart();
20
+ await this.onStart();
21
+ this.afterStart();
22
+ }
23
+
24
+ async stop() {
25
+ this.beforeStop();
26
+ await this.onStop();
27
+ this.afterStop();
28
+ }
29
+
30
+ // Override these methods in subclasses
31
+ async onSetup() {
32
+ // Override in subclasses
33
+ }
34
+
35
+ async onStart() {
36
+ // Override in subclasses
37
+ }
38
+
39
+ async onStop() {
40
+ // Override in subclasses
41
+ }
42
+
43
+ // Hook management methods
44
+ addHook(resource, event, handler) {
45
+ if (!this.hooks.has(resource)) {
46
+ this.hooks.set(resource, new Map());
47
+ }
48
+
49
+ const resourceHooks = this.hooks.get(resource);
50
+ if (!resourceHooks.has(event)) {
51
+ resourceHooks.set(event, []);
52
+ }
53
+
54
+ resourceHooks.get(event).push(handler);
55
+ }
56
+
57
+ removeHook(resource, event, handler) {
58
+ const resourceHooks = this.hooks.get(resource);
59
+ if (resourceHooks && resourceHooks.has(event)) {
60
+ const handlers = resourceHooks.get(event);
61
+ const index = handlers.indexOf(handler);
62
+ if (index > -1) {
63
+ handlers.splice(index, 1);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Enhanced resource method wrapping that supports multiple plugins
69
+ wrapResourceMethod(resource, methodName, wrapper) {
70
+ const originalMethod = resource[methodName];
71
+
72
+ if (!resource._pluginWrappers) {
73
+ resource._pluginWrappers = new Map();
74
+ }
75
+
76
+ if (!resource._pluginWrappers.has(methodName)) {
77
+ resource._pluginWrappers.set(methodName, []);
78
+ }
79
+
80
+ // Store the wrapper
81
+ resource._pluginWrappers.get(methodName).push(wrapper);
82
+
83
+ // Create the wrapped method if it doesn't exist
84
+ if (!resource[`_wrapped_${methodName}`]) {
85
+ resource[`_wrapped_${methodName}`] = originalMethod;
86
+
87
+ // Preserve jest mock if it's a mock function
88
+ const isJestMock = originalMethod && originalMethod._isMockFunction;
89
+
90
+ resource[methodName] = async function(...args) {
91
+ let result = await resource[`_wrapped_${methodName}`](...args);
92
+
93
+ // Apply all wrappers in order
94
+ for (const wrapper of resource._pluginWrappers.get(methodName)) {
95
+ result = await wrapper.call(this, result, args, methodName);
96
+ }
97
+
98
+ return result;
99
+ };
100
+
101
+ // Preserve jest mock properties if it was a mock
102
+ if (isJestMock) {
103
+ Object.setPrototypeOf(resource[methodName], Object.getPrototypeOf(originalMethod));
104
+ Object.assign(resource[methodName], originalMethod);
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Add a middleware to intercept a resource method (Koa/Express style).
111
+ * Middleware signature: async (next, ...args) => { ... }
112
+ * - Chame next(...args) para continuar a cadeia.
113
+ * - Retorne sem chamar next para interromper.
114
+ * - Pode modificar argumentos/resultados.
115
+ */
116
+ addMiddleware(resource, methodName, middleware) {
117
+ if (!resource._pluginMiddlewares) {
118
+ resource._pluginMiddlewares = {};
119
+ }
120
+ if (!resource._pluginMiddlewares[methodName]) {
121
+ resource._pluginMiddlewares[methodName] = [];
122
+ // Wrap the original method only once
123
+ const originalMethod = resource[methodName].bind(resource);
124
+ resource[methodName] = async function(...args) {
125
+ let idx = -1;
126
+ const next = async (...nextArgs) => {
127
+ idx++;
128
+ if (idx < resource._pluginMiddlewares[methodName].length) {
129
+ // Call next middleware
130
+ return await resource._pluginMiddlewares[methodName][idx].call(this, next, ...nextArgs);
131
+ } else {
132
+ // Call original method
133
+ return await originalMethod(...nextArgs);
134
+ }
135
+ };
136
+ return await next(...args);
137
+ };
138
+ }
139
+ resource._pluginMiddlewares[methodName].push(middleware);
140
+ }
141
+
142
+ // Partition-aware helper methods
143
+ getPartitionValues(data, resource) {
144
+ if (!resource.config?.partitions) return {};
145
+
146
+ const partitionValues = {};
147
+ for (const [partitionName, partitionDef] of Object.entries(resource.config.partitions)) {
148
+ if (partitionDef.fields) {
149
+ partitionValues[partitionName] = {};
150
+ for (const [fieldName, rule] of Object.entries(partitionDef.fields)) {
151
+ const value = this.getNestedFieldValue(data, fieldName);
152
+ // Only add field if value exists
153
+ if (value !== null && value !== undefined) {
154
+ partitionValues[partitionName][fieldName] = resource.applyPartitionRule(value, rule);
155
+ }
156
+ }
157
+ } else {
158
+ partitionValues[partitionName] = {};
159
+ }
160
+ }
161
+
162
+ return partitionValues;
163
+ }
164
+
165
+ getNestedFieldValue(data, fieldPath) {
166
+ if (!fieldPath.includes('.')) {
167
+ return data[fieldPath] ?? null;
168
+ }
169
+
170
+ const keys = fieldPath.split('.');
171
+ let value = data;
172
+
173
+ for (const key of keys) {
174
+ if (value && typeof value === 'object' && key in value) {
175
+ value = value[key];
176
+ } else {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ return value ?? null;
182
+ }
183
+
184
+ // Event emission methods
185
+ beforeSetup() {
186
+ this.emit("plugin.beforeSetup", new Date());
187
+ }
188
+
189
+ afterSetup() {
190
+ this.emit("plugin.afterSetup", new Date());
191
+ }
192
+
193
+ beforeStart() {
194
+ this.emit("plugin.beforeStart", new Date());
195
+ }
196
+
197
+ afterStart() {
198
+ this.emit("plugin.afterStart", new Date());
199
+ }
200
+
201
+ beforeStop() {
202
+ this.emit("plugin.beforeStop", new Date());
203
+ }
204
+
205
+ afterStop() {
206
+ this.emit("plugin.afterStop", new Date());
207
+ }
208
+ }
209
+
210
+ export default Plugin;
@@ -0,0 +1,13 @@
1
+ export const PluginObject = {
2
+ setup(database) {
3
+ // TODO: implement me!
4
+ },
5
+
6
+ start() {
7
+ // TODO: implement me!
8
+ },
9
+
10
+ stop() {
11
+ // TODO: implement me!
12
+ },
13
+ }
@@ -0,0 +1,134 @@
1
+ import { createConsumer } from './consumers/index.js';
2
+ import tryFn from "../concerns/try-fn.js";
3
+
4
+ // Exemplo de configuração para SQS:
5
+ // const plugin = new QueueConsumerPlugin({
6
+ // driver: 'sqs',
7
+ // queues: { users: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue' },
8
+ // region: 'us-east-1',
9
+ // credentials: { accessKeyId: '...', secretAccessKey: '...' },
10
+ // poolingInterval: 1000,
11
+ // maxMessages: 10,
12
+ // });
13
+ //
14
+ // Exemplo de configuração para RabbitMQ:
15
+ // const plugin = new QueueConsumerPlugin({
16
+ // driver: 'rabbitmq',
17
+ // queues: { users: 'users-queue' },
18
+ // amqpUrl: 'amqp://user:pass@localhost:5672',
19
+ // prefetch: 10,
20
+ // reconnectInterval: 2000,
21
+ // });
22
+
23
+ export default class QueueConsumerPlugin {
24
+ constructor(options = {}) {
25
+ this.options = options;
26
+ // Novo padrão: consumers = [{ driver, config, consumers: [{ queueUrl, resources, ... }] }]
27
+ this.driversConfig = Array.isArray(options.consumers) ? options.consumers : [];
28
+ this.consumers = [];
29
+ }
30
+
31
+ async setup(database) {
32
+ this.database = database;
33
+
34
+ for (const driverDef of this.driversConfig) {
35
+ const { driver, config: driverConfig = {}, consumers: consumerDefs = [] } = driverDef;
36
+
37
+ // Handle legacy format where config is mixed with driver definition
38
+ if (consumerDefs.length === 0 && driverDef.resources) {
39
+ // Legacy format: { driver: 'sqs', resources: 'users', config: {...} }
40
+ const { resources, driver: defDriver, config: nestedConfig, ...directConfig } = driverDef;
41
+ const resourceList = Array.isArray(resources) ? resources : [resources];
42
+
43
+ // Flatten config - prioritize nested config if it exists, otherwise use direct config
44
+ const flatConfig = nestedConfig ? { ...directConfig, ...nestedConfig } : directConfig;
45
+
46
+ for (const resource of resourceList) {
47
+ const consumer = createConsumer(driver, {
48
+ ...flatConfig,
49
+ onMessage: (msg) => this._handleMessage(msg, resource),
50
+ onError: (err, raw) => this._handleError(err, raw, resource)
51
+ });
52
+
53
+ await consumer.start();
54
+ this.consumers.push(consumer);
55
+ }
56
+ } else {
57
+ // New format: { driver: 'sqs', config: {...}, consumers: [{ resources: 'users', ... }] }
58
+ for (const consumerDef of consumerDefs) {
59
+ const { resources, ...consumerConfig } = consumerDef;
60
+ const resourceList = Array.isArray(resources) ? resources : [resources];
61
+ for (const resource of resourceList) {
62
+ const mergedConfig = { ...driverConfig, ...consumerConfig };
63
+ const consumer = createConsumer(driver, {
64
+ ...mergedConfig,
65
+ onMessage: (msg) => this._handleMessage(msg, resource),
66
+ onError: (err, raw) => this._handleError(err, raw, resource)
67
+ });
68
+ await consumer.start();
69
+ this.consumers.push(consumer);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ async stop() {
77
+ if (!Array.isArray(this.consumers)) this.consumers = [];
78
+ for (const consumer of this.consumers) {
79
+ if (consumer && typeof consumer.stop === 'function') {
80
+ await consumer.stop();
81
+ }
82
+ }
83
+ this.consumers = [];
84
+ }
85
+
86
+ async _handleMessage(msg, configuredResource) {
87
+ const opt = this.options;
88
+ // Permitir resource/action/data tanto na raiz quanto em $body
89
+ // Handle double nesting from SQS parsing
90
+ let body = msg.$body || msg;
91
+ if (body.$body && !body.resource && !body.action && !body.data) {
92
+ // Double nested case - use the inner $body
93
+ body = body.$body;
94
+ }
95
+
96
+ let resource = body.resource || msg.resource;
97
+ let action = body.action || msg.action;
98
+ let data = body.data || msg.data;
99
+
100
+
101
+
102
+ if (!resource) {
103
+ throw new Error('QueueConsumerPlugin: resource not found in message');
104
+ }
105
+ if (!action) {
106
+ throw new Error('QueueConsumerPlugin: action not found in message');
107
+ }
108
+ const resourceObj = this.database.resources[resource];
109
+ if (!resourceObj) throw new Error(`QueueConsumerPlugin: resource '${resource}' not found`);
110
+
111
+ let result;
112
+ const [ok, err, res] = await tryFn(async () => {
113
+ if (action === 'insert') {
114
+ result = await resourceObj.insert(data);
115
+ } else if (action === 'update') {
116
+ const { id: updateId, ...updateAttributes } = data;
117
+ result = await resourceObj.update(updateId, updateAttributes);
118
+ } else if (action === 'delete') {
119
+ result = await resourceObj.delete(data.id);
120
+ } else {
121
+ throw new Error(`QueueConsumerPlugin: unsupported action '${action}'`);
122
+ }
123
+ return result;
124
+ });
125
+
126
+ if (!ok) {
127
+ throw err;
128
+ }
129
+ return res;
130
+ }
131
+
132
+ _handleError(err, raw, resourceName) {
133
+ }
134
+ }