s3db.js 12.0.1 → 12.2.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 (45) hide show
  1. package/README.md +212 -196
  2. package/dist/s3db.cjs.js +1431 -4001
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +1426 -3997
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/entrypoint.js +91 -57
  7. package/package.json +7 -1
  8. package/src/cli/index.js +954 -43
  9. package/src/cli/migration-manager.js +270 -0
  10. package/src/concerns/calculator.js +0 -4
  11. package/src/concerns/metadata-encoding.js +1 -21
  12. package/src/concerns/plugin-storage.js +17 -4
  13. package/src/concerns/typescript-generator.d.ts +171 -0
  14. package/src/concerns/typescript-generator.js +275 -0
  15. package/src/database.class.js +171 -28
  16. package/src/index.js +15 -9
  17. package/src/plugins/api/index.js +0 -1
  18. package/src/plugins/api/routes/resource-routes.js +86 -1
  19. package/src/plugins/api/server.js +79 -3
  20. package/src/plugins/api/utils/openapi-generator.js +195 -5
  21. package/src/plugins/backup/multi-backup-driver.class.js +0 -1
  22. package/src/plugins/backup.plugin.js +7 -14
  23. package/src/plugins/concerns/plugin-dependencies.js +73 -19
  24. package/src/plugins/eventual-consistency/analytics.js +0 -2
  25. package/src/plugins/eventual-consistency/consolidation.js +2 -13
  26. package/src/plugins/eventual-consistency/index.js +0 -1
  27. package/src/plugins/eventual-consistency/install.js +1 -1
  28. package/src/plugins/geo.plugin.js +5 -6
  29. package/src/plugins/importer/index.js +1 -1
  30. package/src/plugins/plugin.class.js +5 -0
  31. package/src/plugins/relation.plugin.js +193 -57
  32. package/src/plugins/replicator.plugin.js +12 -21
  33. package/src/plugins/s3-queue.plugin.js +4 -4
  34. package/src/plugins/scheduler.plugin.js +10 -12
  35. package/src/plugins/state-machine.plugin.js +8 -12
  36. package/src/plugins/tfstate/README.md +1 -1
  37. package/src/plugins/tfstate/errors.js +3 -3
  38. package/src/plugins/tfstate/index.js +41 -67
  39. package/src/plugins/ttl.plugin.js +479 -304
  40. package/src/resource.class.js +263 -61
  41. package/src/schema.class.js +0 -2
  42. package/src/testing/factory.class.js +286 -0
  43. package/src/testing/index.js +15 -0
  44. package/src/testing/seeder.class.js +183 -0
  45. package/dist/s3db-cli.js +0 -55543
@@ -0,0 +1,275 @@
1
+ /**
2
+ * TypeScript Definition Generator
3
+ *
4
+ * Generates .d.ts files from s3db.js resource schemas for type safety and autocomplete.
5
+ *
6
+ * Usage:
7
+ * import { generateTypes } from 's3db.js/typescript-generator';
8
+ * await generateTypes(database, { outputPath: './types/database.d.ts' });
9
+ *
10
+ * Features:
11
+ * - Auto-generates TypeScript interfaces from resource schemas
12
+ * - Type-safe property access (db.resources.users)
13
+ * - Autocomplete for resource methods
14
+ * - Detects typos at compile time (user.emai → error!)
15
+ */
16
+
17
+ import { writeFile, mkdir } from 'fs/promises';
18
+ import { dirname } from 'path';
19
+
20
+ /**
21
+ * Map s3db.js field types to TypeScript types
22
+ * @param {string} fieldType - s3db.js field type
23
+ * @returns {string} TypeScript type
24
+ */
25
+ function mapFieldTypeToTypeScript(fieldType) {
26
+ // Extract base type from validation rules (e.g., "string|required" → "string")
27
+ const baseType = fieldType.split('|')[0].trim();
28
+
29
+ const typeMap = {
30
+ 'string': 'string',
31
+ 'number': 'number',
32
+ 'integer': 'number',
33
+ 'boolean': 'boolean',
34
+ 'array': 'any[]',
35
+ 'object': 'Record<string, any>',
36
+ 'json': 'Record<string, any>',
37
+ 'secret': 'string',
38
+ 'email': 'string',
39
+ 'url': 'string',
40
+ 'date': 'string', // ISO date string
41
+ 'datetime': 'string', // ISO datetime string
42
+ 'ip4': 'string',
43
+ 'ip6': 'string',
44
+ };
45
+
46
+ // Handle embedding:N notation
47
+ if (baseType.startsWith('embedding:')) {
48
+ const dimensions = parseInt(baseType.split(':')[1]);
49
+ return `number[] /* ${dimensions} dimensions */`;
50
+ }
51
+
52
+ return typeMap[baseType] || 'any';
53
+ }
54
+
55
+ /**
56
+ * Check if field is required based on validation rules
57
+ * @param {string} fieldDef - Field definition
58
+ * @returns {boolean} True if required
59
+ */
60
+ function isFieldRequired(fieldDef) {
61
+ if (typeof fieldDef === 'string') {
62
+ return fieldDef.includes('|required');
63
+ }
64
+ if (typeof fieldDef === 'object' && fieldDef.required) {
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Generate TypeScript interface for a resource
72
+ * @param {string} resourceName - Resource name
73
+ * @param {Object} attributes - Resource attributes
74
+ * @param {boolean} timestamps - Whether timestamps are enabled
75
+ * @returns {string} TypeScript interface definition
76
+ */
77
+ function generateResourceInterface(resourceName, attributes, timestamps = false) {
78
+ const interfaceName = toPascalCase(resourceName);
79
+ const lines = [];
80
+
81
+ lines.push(`export interface ${interfaceName} {`);
82
+
83
+ // Add id field (always present)
84
+ lines.push(` /** Resource ID (auto-generated) */`);
85
+ lines.push(` id: string;`);
86
+ lines.push('');
87
+
88
+ // Add user-defined attributes
89
+ for (const [fieldName, fieldDef] of Object.entries(attributes)) {
90
+ const required = isFieldRequired(fieldDef);
91
+ const optional = required ? '' : '?';
92
+
93
+ // Extract type
94
+ let tsType;
95
+ if (typeof fieldDef === 'string') {
96
+ tsType = mapFieldTypeToTypeScript(fieldDef);
97
+ } else if (typeof fieldDef === 'object' && fieldDef.type) {
98
+ tsType = mapFieldTypeToTypeScript(fieldDef.type);
99
+
100
+ // Handle nested objects
101
+ if (fieldDef.type === 'object' && fieldDef.props) {
102
+ tsType = '{\n';
103
+ for (const [propName, propDef] of Object.entries(fieldDef.props)) {
104
+ const propType = typeof propDef === 'string'
105
+ ? mapFieldTypeToTypeScript(propDef)
106
+ : mapFieldTypeToTypeScript(propDef.type);
107
+ const propRequired = isFieldRequired(propDef);
108
+ tsType += ` ${propName}${propRequired ? '' : '?'}: ${propType};\n`;
109
+ }
110
+ tsType += ' }';
111
+ }
112
+
113
+ // Handle arrays with typed items
114
+ if (fieldDef.type === 'array' && fieldDef.items) {
115
+ const itemType = mapFieldTypeToTypeScript(fieldDef.items);
116
+ tsType = `Array<${itemType}>`;
117
+ }
118
+ } else {
119
+ tsType = 'any';
120
+ }
121
+
122
+ // Add JSDoc comment if description exists
123
+ if (fieldDef.description) {
124
+ lines.push(` /** ${fieldDef.description} */`);
125
+ }
126
+
127
+ lines.push(` ${fieldName}${optional}: ${tsType};`);
128
+ }
129
+
130
+ // Add timestamp fields if enabled
131
+ if (timestamps) {
132
+ lines.push('');
133
+ lines.push(` /** Creation timestamp (ISO 8601) */`);
134
+ lines.push(` createdAt: string;`);
135
+ lines.push(` /** Last update timestamp (ISO 8601) */`);
136
+ lines.push(` updatedAt: string;`);
137
+ }
138
+
139
+ lines.push('}');
140
+ lines.push('');
141
+
142
+ return lines.join('\n');
143
+ }
144
+
145
+ /**
146
+ * Convert string to PascalCase
147
+ * @param {string} str - String to convert
148
+ * @returns {string} PascalCase string
149
+ */
150
+ function toPascalCase(str) {
151
+ return str
152
+ .split(/[_-]/)
153
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
154
+ .join('');
155
+ }
156
+
157
+ /**
158
+ * Generate TypeScript definitions for all resources
159
+ * @param {Database} database - s3db.js Database instance
160
+ * @param {Object} options - Generation options
161
+ * @param {string} options.outputPath - Output file path (default: ./types/database.d.ts)
162
+ * @param {string} options.moduleName - Module name for import (default: s3db.js)
163
+ * @param {boolean} options.includeResource - Include Resource class methods (default: true)
164
+ * @returns {Promise<string>} Generated TypeScript definitions
165
+ */
166
+ export async function generateTypes(database, options = {}) {
167
+ const {
168
+ outputPath = './types/database.d.ts',
169
+ moduleName = 's3db.js',
170
+ includeResource = true
171
+ } = options;
172
+
173
+ const lines = [];
174
+
175
+ // File header
176
+ lines.push('/**');
177
+ lines.push(' * Auto-generated TypeScript definitions for s3db.js resources');
178
+ lines.push(' * Generated at: ' + new Date().toISOString());
179
+ lines.push(' * DO NOT EDIT - This file is auto-generated');
180
+ lines.push(' */');
181
+ lines.push('');
182
+
183
+ // Import base types from s3db.js
184
+ if (includeResource) {
185
+ lines.push(`import { Resource, Database } from '${moduleName}';`);
186
+ lines.push('');
187
+ }
188
+
189
+ // Generate interfaces for each resource
190
+ const resourceInterfaces = [];
191
+
192
+ for (const [name, resource] of Object.entries(database.resources)) {
193
+ const attributes = resource.config?.attributes || resource.attributes || {};
194
+ const timestamps = resource.config?.timestamps || false;
195
+
196
+ const interfaceDef = generateResourceInterface(name, attributes, timestamps);
197
+ lines.push(interfaceDef);
198
+
199
+ resourceInterfaces.push({
200
+ name,
201
+ interfaceName: toPascalCase(name),
202
+ resource
203
+ });
204
+ }
205
+
206
+ // Generate ResourceMap interface for db.resources
207
+ lines.push('/**');
208
+ lines.push(' * Typed resource map for property access');
209
+ lines.push(' * @example');
210
+ lines.push(' * const users = db.resources.users; // Type-safe!');
211
+ lines.push(' * const user = await users.get("id"); // Autocomplete works!');
212
+ lines.push(' */');
213
+ lines.push('export interface ResourceMap {');
214
+
215
+ for (const { name, interfaceName } of resourceInterfaces) {
216
+ lines.push(` /** ${interfaceName} resource */`);
217
+ if (includeResource) {
218
+ lines.push(` ${name}: Resource<${interfaceName}>;`);
219
+ } else {
220
+ lines.push(` ${name}: any;`);
221
+ }
222
+ }
223
+
224
+ lines.push('}');
225
+ lines.push('');
226
+
227
+ // Generate Database extension with typed resources property
228
+ if (includeResource) {
229
+ lines.push('/**');
230
+ lines.push(' * Extended Database class with typed resources');
231
+ lines.push(' */');
232
+ lines.push('declare module \'s3db.js\' {');
233
+ lines.push(' interface Database {');
234
+ lines.push(' resources: ResourceMap;');
235
+ lines.push(' }');
236
+ lines.push('');
237
+ lines.push(' interface Resource<T = any> {');
238
+ lines.push(' get(id: string): Promise<T>;');
239
+ lines.push(' getOrNull(id: string): Promise<T | null>;');
240
+ lines.push(' getOrThrow(id: string): Promise<T>;');
241
+ lines.push(' insert(data: Partial<T>): Promise<T>;');
242
+ lines.push(' update(id: string, data: Partial<T>): Promise<T>;');
243
+ lines.push(' patch(id: string, data: Partial<T>): Promise<T>;');
244
+ lines.push(' replace(id: string, data: Partial<T>): Promise<T>;');
245
+ lines.push(' delete(id: string): Promise<void>;');
246
+ lines.push(' list(options?: any): Promise<T[]>;');
247
+ lines.push(' query(filters: Partial<T>, options?: any): Promise<T[]>;');
248
+ lines.push(' validate(data: Partial<T>, options?: any): Promise<{ valid: boolean; errors: any[]; data: T | null }>;');
249
+ lines.push(' }');
250
+ lines.push('}');
251
+ }
252
+
253
+ const content = lines.join('\n');
254
+
255
+ // Write to file if outputPath provided
256
+ if (outputPath) {
257
+ await mkdir(dirname(outputPath), { recursive: true });
258
+ await writeFile(outputPath, content, 'utf-8');
259
+ }
260
+
261
+ return content;
262
+ }
263
+
264
+ /**
265
+ * Generate types and log to console
266
+ * @param {Database} database - s3db.js Database instance
267
+ * @param {Object} options - Generation options
268
+ */
269
+ export async function printTypes(database, options = {}) {
270
+ const types = await generateTypes(database, { ...options, outputPath: null });
271
+ console.log(types);
272
+ return types;
273
+ }
274
+
275
+ export default { generateTypes, printTypes };
@@ -18,19 +18,47 @@ export class Database extends EventEmitter {
18
18
  this.version = "1";
19
19
  // Version is injected during build, fallback to "latest" for development
20
20
  this.s3dbVersion = (() => {
21
- const [ok, err, version] = tryFn(() => (typeof __PACKAGE_VERSION__ !== 'undefined' && __PACKAGE_VERSION__ !== '__PACKAGE_VERSION__'
22
- ? __PACKAGE_VERSION__
21
+ const [ok, err, version] = tryFn(() => (typeof __PACKAGE_VERSION__ !== 'undefined' && __PACKAGE_VERSION__ !== '__PACKAGE_VERSION__'
22
+ ? __PACKAGE_VERSION__
23
23
  : "latest"));
24
24
  return ok ? version : "latest";
25
25
  })();
26
- this.resources = {};
26
+
27
+ // Create Proxy for resources to enable property access (db.resources.users)
28
+ this._resourcesMap = {};
29
+ this.resources = new Proxy(this._resourcesMap, {
30
+ get: (target, prop) => {
31
+ // Allow standard Object methods
32
+ if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'toJSON') {
33
+ return target[prop];
34
+ }
35
+
36
+ // Return resource if exists
37
+ if (target[prop]) {
38
+ return target[prop];
39
+ }
40
+
41
+ // Return undefined for non-existent resources (enables optional chaining)
42
+ return undefined;
43
+ },
44
+
45
+ // Support Object.keys(), Object.entries(), etc.
46
+ ownKeys: (target) => {
47
+ return Object.keys(target);
48
+ },
49
+
50
+ getOwnPropertyDescriptor: (target, prop) => {
51
+ return Object.getOwnPropertyDescriptor(target, prop);
52
+ }
53
+ });
54
+
27
55
  this.savedMetadata = null; // Store loaded metadata for versioning
28
56
  this.options = options;
29
57
  this.verbose = options.verbose || false;
30
58
  this.parallelism = parseInt(options.parallelism + "") || 10;
31
- this.plugins = options.plugins || []; // Keep the original array for backward compatibility
32
- this.pluginRegistry = {}; // Initialize plugins registry as separate object
33
- this.pluginList = options.plugins || []; // Keep the list for backward compatibility
59
+ this.pluginList = options.plugins || [];
60
+ this.pluginRegistry = {};
61
+ this.plugins = this.pluginRegistry; // Alias for plugin registry
34
62
  this.cache = options.cache;
35
63
  this.passphrase = options.passphrase || "secret";
36
64
  this.versioningEnabled = options.versioningEnabled || false;
@@ -181,7 +209,7 @@ export class Database extends EventEmitter {
181
209
  restoredIdSize = versionData.idSize || 22;
182
210
  }
183
211
 
184
- this.resources[name] = new Resource({
212
+ this._resourcesMap[name] = new Resource({
185
213
  name,
186
214
  client: this.client,
187
215
  database: this, // ensure reference
@@ -259,7 +287,7 @@ export class Database extends EventEmitter {
259
287
 
260
288
  // Check for deleted resources
261
289
  for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
262
- if (!this.resources[name]) {
290
+ if (!this._resourcesMap[name]) {
263
291
  const currentVersion = savedResource.currentVersion || 'v1';
264
292
  const versionData = savedResource.versions?.[currentVersion];
265
293
  changes.push({
@@ -894,7 +922,7 @@ export class Database extends EventEmitter {
894
922
  * @returns {boolean} True if resource exists, false otherwise
895
923
  */
896
924
  resourceExists(name) {
897
- return !!this.resources[name];
925
+ return !!this._resourcesMap[name];
898
926
  }
899
927
 
900
928
  /**
@@ -903,17 +931,16 @@ export class Database extends EventEmitter {
903
931
  * @param {string} config.name - Resource name
904
932
  * @param {Object} config.attributes - Resource attributes
905
933
  * @param {string} [config.behavior] - Resource behavior
906
- * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
907
934
  * @returns {Object} Result with exists and hash information
908
935
  */
909
- resourceExistsWithSameHash({ name, attributes, behavior = 'user-managed', partitions = {}, options = {} }) {
910
- if (!this.resources[name]) {
936
+ resourceExistsWithSameHash({ name, attributes, behavior = 'user-managed', partitions = {} }) {
937
+ if (!this._resourcesMap[name]) {
911
938
  return { exists: false, sameHash: false, hash: null };
912
939
  }
913
940
 
914
- const existingResource = this.resources[name];
941
+ const existingResource = this._resourcesMap[name];
915
942
  const existingHash = this.generateDefinitionHash(existingResource.export());
916
-
943
+
917
944
  // Create a mock resource to calculate the new hash
918
945
  const mockResource = new Resource({
919
946
  name,
@@ -923,8 +950,7 @@ export class Database extends EventEmitter {
923
950
  client: this.client,
924
951
  version: existingResource.version,
925
952
  passphrase: this.passphrase,
926
- versioningEnabled: this.versioningEnabled,
927
- ...options
953
+ versioningEnabled: this.versioningEnabled
928
954
  });
929
955
 
930
956
  const newHash = this.generateDefinitionHash(mockResource.export());
@@ -955,13 +981,67 @@ export class Database extends EventEmitter {
955
981
  * @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
956
982
  * @returns {Promise<Resource>} The created or updated resource
957
983
  */
958
- async createResource({ name, attributes, behavior = 'user-managed', hooks, ...config }) {
959
- if (this.resources[name]) {
960
- const existingResource = this.resources[name];
984
+ /**
985
+ * Normalize partitions config from array or object format
986
+ * @param {Array|Object} partitions - Partitions config
987
+ * @param {Object} attributes - Resource attributes
988
+ * @returns {Object} Normalized partitions object
989
+ * @private
990
+ */
991
+ _normalizePartitions(partitions, attributes) {
992
+ // If already an object, return as-is
993
+ if (!Array.isArray(partitions)) {
994
+ return partitions || {};
995
+ }
996
+
997
+ // Transform array into object with auto-generated names
998
+ const normalized = {};
999
+
1000
+ for (const fieldName of partitions) {
1001
+ if (typeof fieldName !== 'string') {
1002
+ throw new Error(`Partition field must be a string, got ${typeof fieldName}`);
1003
+ }
1004
+
1005
+ if (!attributes[fieldName]) {
1006
+ throw new Error(`Partition field '${fieldName}' not found in attributes`);
1007
+ }
1008
+
1009
+ // Generate partition name: byFieldName (capitalize first letter)
1010
+ const partitionName = `by${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
1011
+
1012
+ // Extract field type from attributes
1013
+ const fieldDef = attributes[fieldName];
1014
+ let fieldType = 'string'; // default
1015
+
1016
+ if (typeof fieldDef === 'string') {
1017
+ // String format: "string|required" -> extract "string"
1018
+ fieldType = fieldDef.split('|')[0].trim();
1019
+ } else if (typeof fieldDef === 'object' && fieldDef.type) {
1020
+ // Object format: { type: 'string', required: true }
1021
+ fieldType = fieldDef.type;
1022
+ }
1023
+
1024
+ normalized[partitionName] = {
1025
+ fields: {
1026
+ [fieldName]: fieldType
1027
+ }
1028
+ };
1029
+ }
1030
+
1031
+ return normalized;
1032
+ }
1033
+
1034
+ async createResource({ name, attributes, behavior = 'user-managed', hooks, middlewares, ...config }) {
1035
+ // Normalize partitions (support array shorthand)
1036
+ const normalizedPartitions = this._normalizePartitions(config.partitions, attributes);
1037
+
1038
+ if (this._resourcesMap[name]) {
1039
+ const existingResource = this._resourcesMap[name];
961
1040
  // Update configuration
962
1041
  Object.assign(existingResource.config, {
963
1042
  cache: this.cache,
964
1043
  ...config,
1044
+ partitions: normalizedPartitions
965
1045
  });
966
1046
  if (behavior) {
967
1047
  existingResource.behavior = behavior;
@@ -981,6 +1061,11 @@ export class Database extends EventEmitter {
981
1061
  }
982
1062
  }
983
1063
  }
1064
+ // Apply middlewares if provided
1065
+ if (middlewares) {
1066
+ this._applyMiddlewares(existingResource, middlewares);
1067
+ }
1068
+
984
1069
  // Only upload metadata if hash actually changed
985
1070
  const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
986
1071
  const existingMetadata = this.savedMetadata?.resources?.[name];
@@ -1005,7 +1090,7 @@ export class Database extends EventEmitter {
1005
1090
  observers: [this],
1006
1091
  cache: config.cache !== undefined ? config.cache : this.cache,
1007
1092
  timestamps: config.timestamps !== undefined ? config.timestamps : false,
1008
- partitions: config.partitions || {},
1093
+ partitions: normalizedPartitions,
1009
1094
  paranoid: config.paranoid !== undefined ? config.paranoid : true,
1010
1095
  allNestedObjectsOptional: config.allNestedObjectsOptional !== undefined ? config.allNestedObjectsOptional : true,
1011
1096
  autoDecrypt: config.autoDecrypt !== undefined ? config.autoDecrypt : true,
@@ -1021,18 +1106,76 @@ export class Database extends EventEmitter {
1021
1106
  createdBy: config.createdBy || 'user'
1022
1107
  });
1023
1108
  resource.database = this;
1024
- this.resources[name] = resource;
1109
+ this._resourcesMap[name] = resource;
1110
+
1111
+ // Apply middlewares if provided
1112
+ if (middlewares) {
1113
+ this._applyMiddlewares(resource, middlewares);
1114
+ }
1115
+
1025
1116
  await this.uploadMetadataFile();
1026
1117
  this.emit("s3db.resourceCreated", name);
1027
1118
  return resource;
1028
1119
  }
1029
1120
 
1030
- resource(name) {
1031
- if (!this.resources[name]) {
1032
- return Promise.reject(`resource ${name} does not exist`);
1121
+ /**
1122
+ * Apply middlewares to a resource
1123
+ * @param {Resource} resource - Resource instance
1124
+ * @param {Array|Object} middlewares - Middlewares config
1125
+ * @private
1126
+ */
1127
+ _applyMiddlewares(resource, middlewares) {
1128
+ // Format 1: Array of functions (applies to all methods)
1129
+ if (Array.isArray(middlewares)) {
1130
+ // Apply to all middleware-supported methods
1131
+ const methods = resource._middlewareMethods || [
1132
+ 'get', 'list', 'listIds', 'getAll', 'count', 'page',
1133
+ 'insert', 'update', 'delete', 'deleteMany', 'exists', 'getMany',
1134
+ 'content', 'hasContent', 'query', 'getFromPartition', 'setContent',
1135
+ 'deleteContent', 'replace', 'patch'
1136
+ ];
1137
+
1138
+ for (const method of methods) {
1139
+ for (const middleware of middlewares) {
1140
+ if (typeof middleware === 'function') {
1141
+ resource.useMiddleware(method, middleware);
1142
+ }
1143
+ }
1144
+ }
1145
+ return;
1033
1146
  }
1034
1147
 
1035
- return this.resources[name];
1148
+ // Format 2: Object with method-specific middlewares
1149
+ if (typeof middlewares === 'object' && middlewares !== null) {
1150
+ for (const [method, fns] of Object.entries(middlewares)) {
1151
+ if (method === '*') {
1152
+ // Apply to all methods
1153
+ const methods = resource._middlewareMethods || [
1154
+ 'get', 'list', 'listIds', 'getAll', 'count', 'page',
1155
+ 'insert', 'update', 'delete', 'deleteMany', 'exists', 'getMany',
1156
+ 'content', 'hasContent', 'query', 'getFromPartition', 'setContent',
1157
+ 'deleteContent', 'replace', 'patch'
1158
+ ];
1159
+
1160
+ const middlewareArray = Array.isArray(fns) ? fns : [fns];
1161
+ for (const targetMethod of methods) {
1162
+ for (const middleware of middlewareArray) {
1163
+ if (typeof middleware === 'function') {
1164
+ resource.useMiddleware(targetMethod, middleware);
1165
+ }
1166
+ }
1167
+ }
1168
+ } else {
1169
+ // Apply to specific method
1170
+ const middlewareArray = Array.isArray(fns) ? fns : [fns];
1171
+ for (const middleware of middlewareArray) {
1172
+ if (typeof middleware === 'function') {
1173
+ resource.useMiddleware(method, middleware);
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1036
1179
  }
1037
1180
 
1038
1181
  /**
@@ -1049,14 +1192,14 @@ export class Database extends EventEmitter {
1049
1192
  * @returns {Resource} Resource instance
1050
1193
  */
1051
1194
  async getResource(name) {
1052
- if (!this.resources[name]) {
1195
+ if (!this._resourcesMap[name]) {
1053
1196
  throw new ResourceNotFound({
1054
1197
  bucket: this.client.config.bucket,
1055
1198
  resourceName: name,
1056
1199
  id: name
1057
1200
  });
1058
1201
  }
1059
- return this.resources[name];
1202
+ return this._resourcesMap[name];
1060
1203
  }
1061
1204
 
1062
1205
  /**
@@ -1120,7 +1263,7 @@ export class Database extends EventEmitter {
1120
1263
  });
1121
1264
  }
1122
1265
  // Instead of reassigning, clear in place
1123
- Object.keys(this.resources).forEach(k => delete this.resources[k]);
1266
+ Object.keys(this.resources).forEach(k => delete this._resourcesMap[k]);
1124
1267
  }
1125
1268
 
1126
1269
  // 3. Remove all listeners from the client
package/src/index.js CHANGED
@@ -13,20 +13,26 @@ export { Validator } from './validator.class.js'
13
13
  export { ConnectionString } from './connection-string.class.js'
14
14
 
15
15
  // stream classes
16
- export {
17
- ResourceReader,
18
- ResourceWriter,
19
- ResourceIdsReader,
16
+ export {
17
+ ResourceReader,
18
+ ResourceWriter,
19
+ ResourceIdsReader,
20
20
  ResourceIdsPageReader,
21
21
  streamToString
22
22
  } from './stream/index.js'
23
23
 
24
+ // typescript generation
25
+ export { generateTypes, printTypes } from './concerns/typescript-generator.js'
26
+
27
+ // testing utilities
28
+ export { Factory, Seeder } from './testing/index.js'
29
+
24
30
  // behaviors
25
- export {
26
- behaviors,
27
- getBehavior,
28
- AVAILABLE_BEHAVIORS,
29
- DEFAULT_BEHAVIOR
31
+ export {
32
+ behaviors,
33
+ getBehavior,
34
+ AVAILABLE_BEHAVIORS,
35
+ DEFAULT_BEHAVIOR
30
36
  } from './behaviors/index.js'
31
37
 
32
38
  // default
@@ -56,7 +56,6 @@ export class ApiPlugin extends Plugin {
56
56
  host: options.host || '0.0.0.0',
57
57
  verbose: options.verbose || false,
58
58
 
59
- // API Documentation (supports both new and legacy formats)
60
59
  docs: {
61
60
  enabled: options.docs?.enabled !== false && options.docsEnabled !== false, // Enable by default
62
61
  ui: options.docs?.ui || 'redoc', // 'swagger' or 'redoc' (redoc is prettier!)