s3db.js 12.1.0 → 12.2.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 (43) hide show
  1. package/README.md +212 -196
  2. package/dist/s3db.cjs.js +1041 -1941
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +1039 -1941
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +6 -1
  7. package/src/cli/index.js +954 -43
  8. package/src/cli/migration-manager.js +270 -0
  9. package/src/concerns/calculator.js +0 -4
  10. package/src/concerns/metadata-encoding.js +1 -21
  11. package/src/concerns/plugin-storage.js +17 -4
  12. package/src/concerns/typescript-generator.d.ts +171 -0
  13. package/src/concerns/typescript-generator.js +275 -0
  14. package/src/database.class.js +171 -28
  15. package/src/index.js +15 -9
  16. package/src/plugins/api/index.js +5 -2
  17. package/src/plugins/api/routes/resource-routes.js +86 -1
  18. package/src/plugins/api/server.js +79 -3
  19. package/src/plugins/api/utils/openapi-generator.js +195 -5
  20. package/src/plugins/backup/multi-backup-driver.class.js +0 -1
  21. package/src/plugins/backup.plugin.js +7 -14
  22. package/src/plugins/concerns/plugin-dependencies.js +73 -19
  23. package/src/plugins/eventual-consistency/analytics.js +0 -2
  24. package/src/plugins/eventual-consistency/consolidation.js +2 -13
  25. package/src/plugins/eventual-consistency/index.js +0 -1
  26. package/src/plugins/eventual-consistency/install.js +1 -1
  27. package/src/plugins/geo.plugin.js +5 -6
  28. package/src/plugins/importer/index.js +1 -1
  29. package/src/plugins/index.js +2 -1
  30. package/src/plugins/relation.plugin.js +11 -11
  31. package/src/plugins/replicator.plugin.js +12 -21
  32. package/src/plugins/s3-queue.plugin.js +4 -4
  33. package/src/plugins/scheduler.plugin.js +10 -12
  34. package/src/plugins/state-machine.plugin.js +8 -12
  35. package/src/plugins/tfstate/README.md +1 -1
  36. package/src/plugins/tfstate/errors.js +3 -3
  37. package/src/plugins/tfstate/index.js +41 -67
  38. package/src/plugins/ttl.plugin.js +3 -3
  39. package/src/resource.class.js +263 -61
  40. package/src/schema.class.js +0 -2
  41. package/src/testing/factory.class.js +286 -0
  42. package/src/testing/index.js +15 -0
  43. package/src/testing/seeder.class.js +183 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Factory - Test Data Factory Pattern for s3db.js
3
+ *
4
+ * Simplifies test data creation with:
5
+ * - Automatic field generation
6
+ * - Sequence support
7
+ * - Relationships
8
+ * - Traits/states
9
+ * - Batch creation
10
+ *
11
+ * @example
12
+ * const UserFactory = Factory.define('users', {
13
+ * email: ({ seq }) => `user${seq}@example.com`,
14
+ * name: 'Test User',
15
+ * isActive: true
16
+ * });
17
+ *
18
+ * const user = await UserFactory.create();
19
+ * const users = await UserFactory.createMany(10);
20
+ */
21
+
22
+ export class Factory {
23
+ /**
24
+ * Global sequence counter
25
+ * @private
26
+ */
27
+ static _sequences = new Map();
28
+
29
+ /**
30
+ * Registered factories
31
+ * @private
32
+ */
33
+ static _factories = new Map();
34
+
35
+ /**
36
+ * Database instance (set globally)
37
+ * @private
38
+ */
39
+ static _database = null;
40
+
41
+ /**
42
+ * Create a new factory definition
43
+ * @param {string} resourceName - Resource name
44
+ * @param {Object|Function} definition - Field definitions or function
45
+ * @param {Object} options - Factory options
46
+ * @returns {Factory} Factory instance
47
+ */
48
+ static define(resourceName, definition, options = {}) {
49
+ const factory = new Factory(resourceName, definition, options);
50
+ Factory._factories.set(resourceName, factory);
51
+ return factory;
52
+ }
53
+
54
+ /**
55
+ * Set global database instance
56
+ * @param {Database} database - s3db.js Database instance
57
+ */
58
+ static setDatabase(database) {
59
+ Factory._database = database;
60
+ }
61
+
62
+ /**
63
+ * Get factory by resource name
64
+ * @param {string} resourceName - Resource name
65
+ * @returns {Factory} Factory instance
66
+ */
67
+ static get(resourceName) {
68
+ return Factory._factories.get(resourceName);
69
+ }
70
+
71
+ /**
72
+ * Reset all sequences
73
+ */
74
+ static resetSequences() {
75
+ Factory._sequences.clear();
76
+ }
77
+
78
+ /**
79
+ * Reset all factories
80
+ */
81
+ static reset() {
82
+ Factory._sequences.clear();
83
+ Factory._factories.clear();
84
+ Factory._database = null;
85
+ }
86
+
87
+ /**
88
+ * Constructor
89
+ * @param {string} resourceName - Resource name
90
+ * @param {Object|Function} definition - Field definitions
91
+ * @param {Object} options - Factory options
92
+ */
93
+ constructor(resourceName, definition, options = {}) {
94
+ this.resourceName = resourceName;
95
+ this.definition = definition;
96
+ this.options = options;
97
+ this.traits = new Map();
98
+ this.afterCreateCallbacks = [];
99
+ this.beforeCreateCallbacks = [];
100
+ }
101
+
102
+ /**
103
+ * Get next sequence number
104
+ * @param {string} name - Sequence name (default: factory name)
105
+ * @returns {number} Next sequence number
106
+ */
107
+ sequence(name = this.resourceName) {
108
+ const current = Factory._sequences.get(name) || 0;
109
+ const next = current + 1;
110
+ Factory._sequences.set(name, next);
111
+ return next;
112
+ }
113
+
114
+ /**
115
+ * Define a trait (state variation)
116
+ * @param {string} name - Trait name
117
+ * @param {Object|Function} attributes - Trait attributes
118
+ * @returns {Factory} This factory (for chaining)
119
+ */
120
+ trait(name, attributes) {
121
+ this.traits.set(name, attributes);
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * Register after create callback
127
+ * @param {Function} callback - Callback function
128
+ * @returns {Factory} This factory (for chaining)
129
+ */
130
+ afterCreate(callback) {
131
+ this.afterCreateCallbacks.push(callback);
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * Register before create callback
137
+ * @param {Function} callback - Callback function
138
+ * @returns {Factory} This factory (for chaining)
139
+ */
140
+ beforeCreate(callback) {
141
+ this.beforeCreateCallbacks.push(callback);
142
+ return this;
143
+ }
144
+
145
+ /**
146
+ * Build attributes without creating in database
147
+ * @param {Object} overrides - Override attributes
148
+ * @param {Object} options - Build options
149
+ * @returns {Promise<Object>} Built attributes
150
+ */
151
+ async build(overrides = {}, options = {}) {
152
+ const { traits = [] } = options;
153
+ const seq = this.sequence();
154
+
155
+ // Base attributes
156
+ let attributes = typeof this.definition === 'function'
157
+ ? await this.definition({ seq, factory: this })
158
+ : { ...this.definition };
159
+
160
+ // Apply traits
161
+ for (const traitName of traits) {
162
+ const trait = this.traits.get(traitName);
163
+ if (!trait) {
164
+ throw new Error(`Trait '${traitName}' not found in factory '${this.resourceName}'`);
165
+ }
166
+
167
+ const traitAttrs = typeof trait === 'function'
168
+ ? await trait({ seq, factory: this })
169
+ : trait;
170
+
171
+ attributes = { ...attributes, ...traitAttrs };
172
+ }
173
+
174
+ // Apply overrides
175
+ attributes = { ...attributes, ...overrides };
176
+
177
+ // Resolve functions
178
+ for (const [key, value] of Object.entries(attributes)) {
179
+ if (typeof value === 'function') {
180
+ attributes[key] = await value({ seq, factory: this });
181
+ }
182
+ }
183
+
184
+ return attributes;
185
+ }
186
+
187
+ /**
188
+ * Create resource in database
189
+ * @param {Object} overrides - Override attributes
190
+ * @param {Object} options - Create options
191
+ * @returns {Promise<Object>} Created resource
192
+ */
193
+ async create(overrides = {}, options = {}) {
194
+ const { database = Factory._database } = options;
195
+
196
+ if (!database) {
197
+ throw new Error('Database not set. Use Factory.setDatabase(db) or pass database option');
198
+ }
199
+
200
+ // Build attributes
201
+ let attributes = await this.build(overrides, options);
202
+
203
+ // Before create callbacks
204
+ for (const callback of this.beforeCreateCallbacks) {
205
+ attributes = await callback(attributes) || attributes;
206
+ }
207
+
208
+ // Get resource
209
+ const resource = database.resources[this.resourceName];
210
+ if (!resource) {
211
+ throw new Error(`Resource '${this.resourceName}' not found in database`);
212
+ }
213
+
214
+ // Create in database
215
+ let created = await resource.insert(attributes);
216
+
217
+ // After create callbacks
218
+ for (const callback of this.afterCreateCallbacks) {
219
+ created = await callback(created, { database }) || created;
220
+ }
221
+
222
+ return created;
223
+ }
224
+
225
+ /**
226
+ * Create multiple resources
227
+ * @param {number} count - Number of resources to create
228
+ * @param {Object} overrides - Override attributes
229
+ * @param {Object} options - Create options
230
+ * @returns {Promise<Object[]>} Created resources
231
+ */
232
+ async createMany(count, overrides = {}, options = {}) {
233
+ const resources = [];
234
+
235
+ for (let i = 0; i < count; i++) {
236
+ const resource = await this.create(overrides, options);
237
+ resources.push(resource);
238
+ }
239
+
240
+ return resources;
241
+ }
242
+
243
+ /**
244
+ * Build multiple resources without creating
245
+ * @param {number} count - Number of resources to build
246
+ * @param {Object} overrides - Override attributes
247
+ * @param {Object} options - Build options
248
+ * @returns {Promise<Object[]>} Built resources
249
+ */
250
+ async buildMany(count, overrides = {}, options = {}) {
251
+ const resources = [];
252
+
253
+ for (let i = 0; i < count; i++) {
254
+ const resource = await this.build(overrides, options);
255
+ resources.push(resource);
256
+ }
257
+
258
+ return resources;
259
+ }
260
+
261
+ /**
262
+ * Create with specific traits
263
+ * @param {string|string[]} traits - Trait name(s)
264
+ * @param {Object} overrides - Override attributes
265
+ * @param {Object} options - Create options
266
+ * @returns {Promise<Object>} Created resource
267
+ */
268
+ async createWithTraits(traits, overrides = {}, options = {}) {
269
+ const traitArray = Array.isArray(traits) ? traits : [traits];
270
+ return this.create(overrides, { ...options, traits: traitArray });
271
+ }
272
+
273
+ /**
274
+ * Build with specific traits
275
+ * @param {string|string[]} traits - Trait name(s)
276
+ * @param {Object} overrides - Override attributes
277
+ * @param {Object} options - Build options
278
+ * @returns {Promise<Object>} Built resource
279
+ */
280
+ async buildWithTraits(traits, overrides = {}, options = {}) {
281
+ const traitArray = Array.isArray(traits) ? traits : [traits];
282
+ return this.build(overrides, { ...options, traits: traitArray });
283
+ }
284
+ }
285
+
286
+ export default Factory;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Testing Utilities for s3db.js
3
+ *
4
+ * Provides factory pattern and seeding utilities for tests.
5
+ */
6
+
7
+ import { Factory } from './factory.class.js';
8
+ import { Seeder } from './seeder.class.js';
9
+
10
+ export { Factory, Seeder };
11
+
12
+ export default {
13
+ Factory,
14
+ Seeder
15
+ };
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Seeder - Database Seeding for Tests
3
+ *
4
+ * Provides utilities for seeding test databases with factories.
5
+ *
6
+ * @example
7
+ * const seeder = new Seeder(database);
8
+ *
9
+ * await seeder.seed({
10
+ * users: 10,
11
+ * posts: 50,
12
+ * comments: 100
13
+ * });
14
+ *
15
+ * await seeder.truncate(['users', 'posts']);
16
+ */
17
+
18
+ import { Factory } from './factory.class.js';
19
+
20
+ export class Seeder {
21
+ /**
22
+ * Constructor
23
+ * @param {Database} database - s3db.js Database instance
24
+ * @param {Object} options - Seeder options
25
+ */
26
+ constructor(database, options = {}) {
27
+ this.database = database;
28
+ this.options = options;
29
+ this.verbose = options.verbose !== false;
30
+ }
31
+
32
+ /**
33
+ * Log message (if verbose)
34
+ * @param {string} message - Message to log
35
+ * @private
36
+ */
37
+ log(message) {
38
+ if (this.verbose) {
39
+ console.log(`[Seeder] ${message}`);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Seed resources using factories
45
+ * @param {Object} specs - Seed specifications { resourceName: count }
46
+ * @returns {Promise<Object>} Created resources by resource name
47
+ *
48
+ * @example
49
+ * const created = await seeder.seed({
50
+ * users: 10,
51
+ * posts: 50
52
+ * });
53
+ */
54
+ async seed(specs) {
55
+ const created = {};
56
+
57
+ for (const [resourceName, count] of Object.entries(specs)) {
58
+ this.log(`Seeding ${count} ${resourceName}...`);
59
+
60
+ const factory = Factory.get(resourceName);
61
+ if (!factory) {
62
+ throw new Error(`Factory for '${resourceName}' not found. Define it with Factory.define()`);
63
+ }
64
+
65
+ created[resourceName] = await factory.createMany(count, {}, { database: this.database });
66
+
67
+ this.log(`✅ Created ${count} ${resourceName}`);
68
+ }
69
+
70
+ return created;
71
+ }
72
+
73
+ /**
74
+ * Seed with custom callback
75
+ * @param {Function} callback - Seeding callback
76
+ * @returns {Promise<any>} Result of callback
77
+ *
78
+ * @example
79
+ * await seeder.call(async (db) => {
80
+ * const user = await UserFactory.create();
81
+ * const posts = await PostFactory.createMany(5, { userId: user.id });
82
+ * return { user, posts };
83
+ * });
84
+ */
85
+ async call(callback) {
86
+ this.log('Running custom seeder...');
87
+ const result = await callback(this.database);
88
+ this.log('✅ Custom seeder completed');
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * Truncate resources (delete all data)
94
+ * @param {string[]} resourceNames - Resource names to truncate
95
+ * @returns {Promise<void>}
96
+ *
97
+ * @example
98
+ * await seeder.truncate(['users', 'posts']);
99
+ */
100
+ async truncate(resourceNames) {
101
+ for (const resourceName of resourceNames) {
102
+ this.log(`Truncating ${resourceName}...`);
103
+
104
+ const resource = this.database.resources[resourceName];
105
+ if (!resource) {
106
+ this.log(`⚠️ Resource '${resourceName}' not found, skipping`);
107
+ continue;
108
+ }
109
+
110
+ // List all IDs
111
+ const ids = await resource.listIds();
112
+
113
+ // Delete all
114
+ if (ids.length > 0) {
115
+ await resource.deleteMany(ids);
116
+ this.log(`✅ Deleted ${ids.length} ${resourceName}`);
117
+ } else {
118
+ this.log(`✅ ${resourceName} already empty`);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Truncate all resources
125
+ * @returns {Promise<void>}
126
+ */
127
+ async truncateAll() {
128
+ const resourceNames = Object.keys(this.database.resources);
129
+ await this.truncate(resourceNames);
130
+ }
131
+
132
+ /**
133
+ * Run multiple seeders in order
134
+ * @param {Function[]} seeders - Array of seeder functions
135
+ * @returns {Promise<Object[]>} Results of each seeder
136
+ *
137
+ * @example
138
+ * await seeder.run([
139
+ * async (db) => await UserFactory.createMany(10),
140
+ * async (db) => await PostFactory.createMany(50)
141
+ * ]);
142
+ */
143
+ async run(seeders) {
144
+ const results = [];
145
+
146
+ for (const seederFn of seeders) {
147
+ this.log(`Running seeder ${seederFn.name || 'anonymous'}...`);
148
+ const result = await seederFn(this.database);
149
+ results.push(result);
150
+ this.log(`✅ Completed ${seederFn.name || 'anonymous'}`);
151
+ }
152
+
153
+ return results;
154
+ }
155
+
156
+ /**
157
+ * Seed and return specific resources
158
+ * @param {Object} specs - Seed specifications
159
+ * @returns {Promise<Object>} Created resources
160
+ *
161
+ * @example
162
+ * const { users, posts } = await seeder.seedAndReturn({
163
+ * users: 5,
164
+ * posts: 10
165
+ * });
166
+ */
167
+ async seedAndReturn(specs) {
168
+ return await this.seed(specs);
169
+ }
170
+
171
+ /**
172
+ * Reset database (truncate all and reset sequences)
173
+ * @returns {Promise<void>}
174
+ */
175
+ async reset() {
176
+ this.log('Resetting database...');
177
+ await this.truncateAll();
178
+ Factory.resetSequences();
179
+ this.log('✅ Database reset complete');
180
+ }
181
+ }
182
+
183
+ export default Seeder;