s3db.js 12.1.0 → 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 (42) hide show
  1. package/README.md +212 -196
  2. package/dist/s3db.cjs.js +1286 -226
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.es.js +1284 -226
  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 +0 -1
  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/relation.plugin.js +11 -11
  30. package/src/plugins/replicator.plugin.js +12 -21
  31. package/src/plugins/s3-queue.plugin.js +4 -4
  32. package/src/plugins/scheduler.plugin.js +10 -12
  33. package/src/plugins/state-machine.plugin.js +8 -12
  34. package/src/plugins/tfstate/README.md +1 -1
  35. package/src/plugins/tfstate/errors.js +3 -3
  36. package/src/plugins/tfstate/index.js +41 -67
  37. package/src/plugins/ttl.plugin.js +3 -3
  38. package/src/resource.class.js +263 -61
  39. package/src/schema.class.js +0 -2
  40. package/src/testing/factory.class.js +286 -0
  41. package/src/testing/index.js +15 -0
  42. package/src/testing/seeder.class.js +183 -0
package/dist/s3db.es.js CHANGED
@@ -7,7 +7,7 @@ import { swaggerUI } from '@hono/swagger-ui';
7
7
  import { mkdir, copyFile, unlink, stat, access, readdir, writeFile, readFile, rm, watch } from 'fs/promises';
8
8
  import fs, { createReadStream, createWriteStream, realpathSync as realpathSync$1, readlinkSync, readdirSync, readdir as readdir$2, lstatSync, existsSync } from 'fs';
9
9
  import { pipeline } from 'stream/promises';
10
- import path$1, { join } from 'path';
10
+ import path$1, { join, dirname } from 'path';
11
11
  import { Transform, Writable } from 'stream';
12
12
  import zlib from 'node:zlib';
13
13
  import os from 'os';
@@ -209,8 +209,6 @@ function calculateUTF8Bytes(str) {
209
209
  function clearUTF8Memory() {
210
210
  utf8BytesMemory.clear();
211
211
  }
212
- const clearUTF8Memo = clearUTF8Memory;
213
- const clearUTF8Cache = clearUTF8Memory;
214
212
  function calculateAttributeNamesSize(mappedObject) {
215
213
  let totalSize = 0;
216
214
  for (const key of Object.keys(mappedObject)) {
@@ -1604,18 +1602,6 @@ function metadataDecode(value) {
1604
1602
  }
1605
1603
  }
1606
1604
  }
1607
- const len = value.length;
1608
- if (len > 0 && len % 4 === 0) {
1609
- if (/^[A-Za-z0-9+/]+=*$/.test(value)) {
1610
- try {
1611
- const decoded = Buffer.from(value, "base64").toString("utf8");
1612
- if (/[^\x00-\x7F]/.test(decoded) && Buffer.from(decoded, "utf8").toString("base64") === value) {
1613
- return decoded;
1614
- }
1615
- } catch {
1616
- }
1617
- }
1618
- }
1619
1605
  return value;
1620
1606
  }
1621
1607
 
@@ -1702,11 +1688,22 @@ class PluginStorage {
1702
1688
  }
1703
1689
  }
1704
1690
  /**
1705
- * Alias for set() to maintain backward compatibility
1706
- * @deprecated Use set() instead
1691
+ * Batch set multiple items
1692
+ *
1693
+ * @param {Array<{key: string, data: Object, options?: Object}>} items - Items to save
1694
+ * @returns {Promise<Array<{ok: boolean, key: string, error?: Error}>>} Results
1707
1695
  */
1708
- async put(key, data, options = {}) {
1709
- return this.set(key, data, options);
1696
+ async batchSet(items) {
1697
+ const results = [];
1698
+ for (const item of items) {
1699
+ try {
1700
+ await this.set(item.key, item.data, item.options || {});
1701
+ results.push({ ok: true, key: item.key });
1702
+ } catch (error) {
1703
+ results.push({ ok: false, key: item.key, error });
1704
+ }
1705
+ }
1706
+ return results;
1710
1707
  }
1711
1708
  /**
1712
1709
  * Get data with automatic metadata decoding and TTL check
@@ -2821,6 +2818,57 @@ function createResourceRoutes(resource, version, config = {}) {
2821
2818
  }
2822
2819
  return app;
2823
2820
  }
2821
+ function createRelationalRoutes(sourceResource, relationName, relationConfig, version) {
2822
+ const app = new Hono();
2823
+ const resourceName = sourceResource.name;
2824
+ const relatedResourceName = relationConfig.resource;
2825
+ app.get("/:id", asyncHandler(async (c) => {
2826
+ const id = c.req.param("id");
2827
+ const query = c.req.query();
2828
+ const source = await sourceResource.get(id);
2829
+ if (!source) {
2830
+ const response = notFound(resourceName, id);
2831
+ return c.json(response, response._status);
2832
+ }
2833
+ const result = await sourceResource.get(id, {
2834
+ include: [relationName]
2835
+ });
2836
+ const relatedData = result[relationName];
2837
+ if (!relatedData) {
2838
+ if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
2839
+ const response = list([], {
2840
+ total: 0,
2841
+ page: 1,
2842
+ pageSize: 100,
2843
+ pageCount: 0
2844
+ });
2845
+ return c.json(response, response._status);
2846
+ } else {
2847
+ const response = notFound(relatedResourceName, "related resource");
2848
+ return c.json(response, response._status);
2849
+ }
2850
+ }
2851
+ if (relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany") {
2852
+ const items = Array.isArray(relatedData) ? relatedData : [relatedData];
2853
+ const limit = parseInt(query.limit) || 100;
2854
+ const offset = parseInt(query.offset) || 0;
2855
+ const paginatedItems = items.slice(offset, offset + limit);
2856
+ const response = list(paginatedItems, {
2857
+ total: items.length,
2858
+ page: Math.floor(offset / limit) + 1,
2859
+ pageSize: limit,
2860
+ pageCount: Math.ceil(items.length / limit)
2861
+ });
2862
+ c.header("X-Total-Count", items.length.toString());
2863
+ c.header("X-Page-Count", Math.ceil(items.length / limit).toString());
2864
+ return c.json(response, response._status);
2865
+ } else {
2866
+ const response = success(relatedData);
2867
+ return c.json(response, response._status);
2868
+ }
2869
+ }));
2870
+ return app;
2871
+ }
2824
2872
 
2825
2873
  function mapFieldTypeToOpenAPI(fieldType) {
2826
2874
  const type = fieldType.split("|")[0].trim();
@@ -2891,6 +2939,8 @@ function generateResourceSchema(resource) {
2891
2939
  const properties = {};
2892
2940
  const required = [];
2893
2941
  const attributes = resource.config?.attributes || resource.attributes || {};
2942
+ const resourceDescription = resource.config?.description;
2943
+ const attributeDescriptions = typeof resourceDescription === "object" ? resourceDescription.attributes || {} : {};
2894
2944
  properties.id = {
2895
2945
  type: "string",
2896
2946
  description: "Unique identifier for the resource",
@@ -2902,7 +2952,7 @@ function generateResourceSchema(resource) {
2902
2952
  const baseType = mapFieldTypeToOpenAPI(fieldDef.type);
2903
2953
  properties[fieldName] = {
2904
2954
  ...baseType,
2905
- description: fieldDef.description || void 0
2955
+ description: fieldDef.description || attributeDescriptions[fieldName] || void 0
2906
2956
  };
2907
2957
  if (fieldDef.required) {
2908
2958
  required.push(fieldName);
@@ -2922,7 +2972,8 @@ function generateResourceSchema(resource) {
2922
2972
  const rules = extractValidationRules(fieldDef);
2923
2973
  properties[fieldName] = {
2924
2974
  ...baseType,
2925
- ...rules
2975
+ ...rules,
2976
+ description: attributeDescriptions[fieldName] || void 0
2926
2977
  };
2927
2978
  if (rules.required) {
2928
2979
  required.push(fieldName);
@@ -3504,6 +3555,98 @@ The response includes pagination metadata in the \`pagination\` object with tota
3504
3555
  }
3505
3556
  return paths;
3506
3557
  }
3558
+ function generateRelationalPaths(resource, relationName, relationConfig, version, relatedSchema) {
3559
+ const resourceName = resource.name;
3560
+ const basePath = `/${version}/${resourceName}/{id}/${relationName}`;
3561
+ relationConfig.resource;
3562
+ const isToMany = relationConfig.type === "hasMany" || relationConfig.type === "belongsToMany";
3563
+ const paths = {};
3564
+ paths[basePath] = {
3565
+ get: {
3566
+ tags: [resourceName],
3567
+ summary: `Get ${relationName} of ${resourceName}`,
3568
+ description: `Retrieve ${relationName} (${relationConfig.type}) associated with this ${resourceName}. This endpoint uses the RelationPlugin to efficiently load related data` + (relationConfig.partitionHint ? ` via the '${relationConfig.partitionHint}' partition.` : "."),
3569
+ parameters: [
3570
+ {
3571
+ name: "id",
3572
+ in: "path",
3573
+ required: true,
3574
+ description: `${resourceName} ID`,
3575
+ schema: { type: "string" }
3576
+ },
3577
+ ...isToMany ? [
3578
+ {
3579
+ name: "limit",
3580
+ in: "query",
3581
+ description: "Maximum number of items to return",
3582
+ schema: { type: "integer", default: 100, minimum: 1, maximum: 1e3 }
3583
+ },
3584
+ {
3585
+ name: "offset",
3586
+ in: "query",
3587
+ description: "Number of items to skip",
3588
+ schema: { type: "integer", default: 0, minimum: 0 }
3589
+ }
3590
+ ] : []
3591
+ ],
3592
+ responses: {
3593
+ 200: {
3594
+ description: "Successful response",
3595
+ content: {
3596
+ "application/json": {
3597
+ schema: isToMany ? {
3598
+ type: "object",
3599
+ properties: {
3600
+ success: { type: "boolean", example: true },
3601
+ data: {
3602
+ type: "array",
3603
+ items: relatedSchema
3604
+ },
3605
+ pagination: {
3606
+ type: "object",
3607
+ properties: {
3608
+ total: { type: "integer" },
3609
+ page: { type: "integer" },
3610
+ pageSize: { type: "integer" },
3611
+ pageCount: { type: "integer" }
3612
+ }
3613
+ }
3614
+ }
3615
+ } : {
3616
+ type: "object",
3617
+ properties: {
3618
+ success: { type: "boolean", example: true },
3619
+ data: relatedSchema
3620
+ }
3621
+ }
3622
+ }
3623
+ },
3624
+ ...isToMany ? {
3625
+ headers: {
3626
+ "X-Total-Count": {
3627
+ description: "Total number of related records",
3628
+ schema: { type: "integer" }
3629
+ },
3630
+ "X-Page-Count": {
3631
+ description: "Total number of pages",
3632
+ schema: { type: "integer" }
3633
+ }
3634
+ }
3635
+ } : {}
3636
+ },
3637
+ 404: {
3638
+ description: `${resourceName} not found` + (isToMany ? "" : " or no related resource exists"),
3639
+ content: {
3640
+ "application/json": {
3641
+ schema: { $ref: "#/components/schemas/Error" }
3642
+ }
3643
+ }
3644
+ }
3645
+ }
3646
+ }
3647
+ };
3648
+ return paths;
3649
+ }
3507
3650
  function generateOpenAPISpec(database, config = {}) {
3508
3651
  const {
3509
3652
  title = "s3db.js API",
@@ -3513,12 +3656,33 @@ function generateOpenAPISpec(database, config = {}) {
3513
3656
  auth = {},
3514
3657
  resources: resourceConfigs = {}
3515
3658
  } = config;
3659
+ const resourcesTableRows = [];
3660
+ for (const [name, resource] of Object.entries(database.resources)) {
3661
+ if (name.startsWith("plg_") && !resourceConfigs[name]) {
3662
+ continue;
3663
+ }
3664
+ const version2 = resource.config?.currentVersion || resource.version || "v1";
3665
+ const resourceDescription = resource.config?.description;
3666
+ const descText = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || "No description";
3667
+ resourcesTableRows.push(`| ${name} | ${descText} | \`/${version2}/${name}\` |`);
3668
+ }
3669
+ const enhancedDescription = `${description}
3670
+
3671
+ ## Available Resources
3672
+
3673
+ | Resource | Description | Base Path |
3674
+ |----------|-------------|-----------|
3675
+ ${resourcesTableRows.join("\n")}
3676
+
3677
+ ---
3678
+
3679
+ For detailed information about each endpoint, see the sections below.`;
3516
3680
  const spec = {
3517
3681
  openapi: "3.1.0",
3518
3682
  info: {
3519
3683
  title,
3520
3684
  version,
3521
- description,
3685
+ description: enhancedDescription,
3522
3686
  contact: {
3523
3687
  name: "s3db.js",
3524
3688
  url: "https://github.com/forattini-dev/s3db.js"
@@ -3606,6 +3770,7 @@ function generateOpenAPISpec(database, config = {}) {
3606
3770
  };
3607
3771
  }
3608
3772
  const resources = database.resources;
3773
+ const relationsPlugin = database.plugins?.relation || database.plugins?.RelationPlugin || null;
3609
3774
  for (const [name, resource] of Object.entries(resources)) {
3610
3775
  if (name.startsWith("plg_") && !resourceConfigs[name]) {
3611
3776
  continue;
@@ -3617,11 +3782,38 @@ function generateOpenAPISpec(database, config = {}) {
3617
3782
  const version2 = resource.config?.currentVersion || resource.version || "v1";
3618
3783
  const paths = generateResourcePaths(resource, version2, config2);
3619
3784
  Object.assign(spec.paths, paths);
3785
+ const resourceDescription = resource.config?.description;
3786
+ const tagDescription = typeof resourceDescription === "object" ? resourceDescription.resource : resourceDescription || `Operations for ${name} resource`;
3620
3787
  spec.tags.push({
3621
3788
  name,
3622
- description: `Operations for ${name} resource`
3789
+ description: tagDescription
3623
3790
  });
3624
3791
  spec.components.schemas[name] = generateResourceSchema(resource);
3792
+ if (relationsPlugin && relationsPlugin.relations && relationsPlugin.relations[name]) {
3793
+ const relationsDef = relationsPlugin.relations[name];
3794
+ for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
3795
+ if (relationConfig.type === "belongsTo") {
3796
+ continue;
3797
+ }
3798
+ const exposeRelation = config2?.relations?.[relationName]?.expose !== false;
3799
+ if (!exposeRelation) {
3800
+ continue;
3801
+ }
3802
+ const relatedResource = database.resources[relationConfig.resource];
3803
+ if (!relatedResource) {
3804
+ continue;
3805
+ }
3806
+ const relatedSchema = generateResourceSchema(relatedResource);
3807
+ const relationalPaths = generateRelationalPaths(
3808
+ resource,
3809
+ relationName,
3810
+ relationConfig,
3811
+ version2,
3812
+ relatedSchema
3813
+ );
3814
+ Object.assign(spec.paths, relationalPaths);
3815
+ }
3816
+ }
3625
3817
  }
3626
3818
  if (auth.jwt?.enabled || auth.apiKey?.enabled || auth.basic?.enabled) {
3627
3819
  spec.paths["/auth/login"] = {
@@ -3935,6 +4127,7 @@ class ApiServer {
3935
4127
  this.server = null;
3936
4128
  this.isRunning = false;
3937
4129
  this.openAPISpec = null;
4130
+ this.relationsPlugin = this.options.database?.plugins?.relation || this.options.database?.plugins?.RelationPlugin || null;
3938
4131
  this._setupRoutes();
3939
4132
  }
3940
4133
  /**
@@ -4045,6 +4238,9 @@ class ApiServer {
4045
4238
  }
4046
4239
  }
4047
4240
  this._setupResourceRoutes();
4241
+ if (this.relationsPlugin) {
4242
+ this._setupRelationalRoutes();
4243
+ }
4048
4244
  this.app.onError((err, c) => {
4049
4245
  return errorHandler(err, c);
4050
4246
  });
@@ -4085,6 +4281,53 @@ class ApiServer {
4085
4281
  }
4086
4282
  }
4087
4283
  }
4284
+ /**
4285
+ * Setup relational routes (when RelationPlugin is active)
4286
+ * @private
4287
+ */
4288
+ _setupRelationalRoutes() {
4289
+ if (!this.relationsPlugin || !this.relationsPlugin.relations) {
4290
+ return;
4291
+ }
4292
+ const { database } = this.options;
4293
+ const relations = this.relationsPlugin.relations;
4294
+ if (this.options.verbose) {
4295
+ console.log("[API Plugin] Setting up relational routes...");
4296
+ }
4297
+ for (const [resourceName, relationsDef] of Object.entries(relations)) {
4298
+ const resource = database.resources[resourceName];
4299
+ if (!resource) {
4300
+ if (this.options.verbose) {
4301
+ console.warn(`[API Plugin] Resource '${resourceName}' not found for relational routes`);
4302
+ }
4303
+ continue;
4304
+ }
4305
+ if (resourceName.startsWith("plg_") && !this.options.resources[resourceName]) {
4306
+ continue;
4307
+ }
4308
+ const version = resource.config?.currentVersion || resource.version || "v1";
4309
+ for (const [relationName, relationConfig] of Object.entries(relationsDef)) {
4310
+ if (relationConfig.type === "belongsTo") {
4311
+ continue;
4312
+ }
4313
+ const resourceConfig = this.options.resources[resourceName];
4314
+ const exposeRelation = resourceConfig?.relations?.[relationName]?.expose !== false;
4315
+ if (!exposeRelation) {
4316
+ continue;
4317
+ }
4318
+ const relationalApp = createRelationalRoutes(
4319
+ resource,
4320
+ relationName,
4321
+ relationConfig);
4322
+ this.app.route(`/${version}/${resourceName}/:id/${relationName}`, relationalApp);
4323
+ if (this.options.verbose) {
4324
+ console.log(
4325
+ `[API Plugin] Mounted relational route: /${version}/${resourceName}/:id/${relationName} (${relationConfig.type} -> ${relationConfig.resource})`
4326
+ );
4327
+ }
4328
+ }
4329
+ }
4330
+ }
4088
4331
  /**
4089
4332
  * Start the server
4090
4333
  * @returns {Promise<void>}
@@ -4173,81 +4416,97 @@ class ApiServer {
4173
4416
  const PLUGIN_DEPENDENCIES = {
4174
4417
  "postgresql-replicator": {
4175
4418
  name: "PostgreSQL Replicator",
4419
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
4176
4420
  dependencies: {
4177
4421
  "pg": {
4178
4422
  version: "^8.0.0",
4179
4423
  description: "PostgreSQL client for Node.js",
4180
- installCommand: "pnpm add pg"
4424
+ installCommand: "pnpm add pg",
4425
+ npmUrl: "https://www.npmjs.com/package/pg"
4181
4426
  }
4182
4427
  }
4183
4428
  },
4184
4429
  "bigquery-replicator": {
4185
4430
  name: "BigQuery Replicator",
4431
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
4186
4432
  dependencies: {
4187
4433
  "@google-cloud/bigquery": {
4188
4434
  version: "^7.0.0",
4189
4435
  description: "Google Cloud BigQuery SDK",
4190
- installCommand: "pnpm add @google-cloud/bigquery"
4436
+ installCommand: "pnpm add @google-cloud/bigquery",
4437
+ npmUrl: "https://www.npmjs.com/package/@google-cloud/bigquery"
4191
4438
  }
4192
4439
  }
4193
4440
  },
4194
4441
  "sqs-replicator": {
4195
4442
  name: "SQS Replicator",
4443
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md",
4196
4444
  dependencies: {
4197
4445
  "@aws-sdk/client-sqs": {
4198
4446
  version: "^3.0.0",
4199
4447
  description: "AWS SDK for SQS",
4200
- installCommand: "pnpm add @aws-sdk/client-sqs"
4448
+ installCommand: "pnpm add @aws-sdk/client-sqs",
4449
+ npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
4201
4450
  }
4202
4451
  }
4203
4452
  },
4204
4453
  "sqs-consumer": {
4205
4454
  name: "SQS Queue Consumer",
4455
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
4206
4456
  dependencies: {
4207
4457
  "@aws-sdk/client-sqs": {
4208
4458
  version: "^3.0.0",
4209
4459
  description: "AWS SDK for SQS",
4210
- installCommand: "pnpm add @aws-sdk/client-sqs"
4460
+ installCommand: "pnpm add @aws-sdk/client-sqs",
4461
+ npmUrl: "https://www.npmjs.com/package/@aws-sdk/client-sqs"
4211
4462
  }
4212
4463
  }
4213
4464
  },
4214
4465
  "rabbitmq-consumer": {
4215
4466
  name: "RabbitMQ Queue Consumer",
4467
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue-consumer.md",
4216
4468
  dependencies: {
4217
4469
  "amqplib": {
4218
4470
  version: "^0.10.0",
4219
4471
  description: "AMQP 0-9-1 library for RabbitMQ",
4220
- installCommand: "pnpm add amqplib"
4472
+ installCommand: "pnpm add amqplib",
4473
+ npmUrl: "https://www.npmjs.com/package/amqplib"
4221
4474
  }
4222
4475
  }
4223
4476
  },
4224
4477
  "tfstate-plugin": {
4225
- name: "Terraform State Plugin",
4478
+ name: "Tfstate Plugin",
4479
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/tfstate.md",
4226
4480
  dependencies: {
4227
4481
  "node-cron": {
4228
4482
  version: "^4.0.0",
4229
4483
  description: "Cron job scheduler for auto-sync functionality",
4230
- installCommand: "pnpm add node-cron"
4484
+ installCommand: "pnpm add node-cron",
4485
+ npmUrl: "https://www.npmjs.com/package/node-cron"
4231
4486
  }
4232
4487
  }
4233
4488
  },
4234
4489
  "api-plugin": {
4235
4490
  name: "API Plugin",
4491
+ docsUrl: "https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/api.md",
4236
4492
  dependencies: {
4237
4493
  "hono": {
4238
4494
  version: "^4.0.0",
4239
4495
  description: "Ultra-light HTTP server framework",
4240
- installCommand: "pnpm add hono"
4496
+ installCommand: "pnpm add hono",
4497
+ npmUrl: "https://www.npmjs.com/package/hono"
4241
4498
  },
4242
4499
  "@hono/node-server": {
4243
4500
  version: "^1.0.0",
4244
4501
  description: "Node.js adapter for Hono",
4245
- installCommand: "pnpm add @hono/node-server"
4502
+ installCommand: "pnpm add @hono/node-server",
4503
+ npmUrl: "https://www.npmjs.com/package/@hono/node-server"
4246
4504
  },
4247
4505
  "@hono/swagger-ui": {
4248
4506
  version: "^0.4.0",
4249
4507
  description: "Swagger UI integration for Hono",
4250
- installCommand: "pnpm add @hono/swagger-ui"
4508
+ installCommand: "pnpm add @hono/swagger-ui",
4509
+ npmUrl: "https://www.npmjs.com/package/@hono/swagger-ui"
4251
4510
  }
4252
4511
  }
4253
4512
  }
@@ -4333,21 +4592,55 @@ async function requirePluginDependency(pluginId, options = {}) {
4333
4592
  }
4334
4593
  const valid = missing.length === 0 && incompatible.length === 0;
4335
4594
  if (!valid && throwOnError) {
4595
+ const depCount = Object.keys(pluginDef.dependencies).length;
4596
+ const missingCount = missing.length;
4597
+ const incompatCount = incompatible.length;
4336
4598
  const errorMsg = [
4337
- `
4338
- ${pluginDef.name} - Missing dependencies detected!
4339
- `,
4340
- `Plugin ID: ${pluginId}`,
4341
4599
  "",
4600
+ "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
4601
+ `\u2551 \u274C ${pluginDef.name} - Missing Dependencies \u2551`,
4602
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
4603
+ "",
4604
+ `\u{1F4E6} Plugin: ${pluginId}`,
4605
+ `\u{1F4CA} Status: ${depCount - missingCount - incompatCount}/${depCount} dependencies satisfied`,
4606
+ "",
4607
+ "\u{1F50D} Dependency Status:",
4608
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
4342
4609
  ...messages,
4343
4610
  "",
4344
- "Quick fix - Run all install commands:",
4345
- Object.values(pluginDef.dependencies).map((dep) => ` ${dep.installCommand}`).join("\n"),
4611
+ "\u{1F680} Quick Fix - Install Missing Dependencies:",
4612
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
4613
+ "",
4614
+ " Option 1: Install individually",
4615
+ ...Object.entries(pluginDef.dependencies).filter(([pkg]) => missing.includes(pkg) || incompatible.includes(pkg)).map(([pkg, info]) => ` ${info.installCommand}`),
4616
+ "",
4617
+ " Option 2: Install all at once",
4618
+ ` pnpm add ${Object.keys(pluginDef.dependencies).join(" ")}`,
4619
+ "",
4620
+ "\u{1F4DA} Documentation:",
4621
+ ` ${pluginDef.docsUrl}`,
4622
+ "",
4623
+ "\u{1F4A1} Troubleshooting:",
4624
+ " \u2022 If packages are installed but not detected, try:",
4625
+ " 1. Delete node_modules and reinstall: rm -rf node_modules && pnpm install",
4626
+ " 2. Check Node.js version: node --version (requires Node 18+)",
4627
+ " 3. Verify pnpm version: pnpm --version (requires pnpm 8+)",
4346
4628
  "",
4347
- "Or install all peer dependencies at once:",
4348
- ` pnpm add ${Object.keys(pluginDef.dependencies).join(" ")}`
4629
+ " \u2022 Still having issues? Check:",
4630
+ " - Package.json has correct dependencies listed",
4631
+ " - No conflicting versions in pnpm-lock.yaml",
4632
+ " - File permissions (especially in node_modules/)",
4633
+ "",
4634
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
4635
+ ""
4349
4636
  ].join("\n");
4350
- throw new Error(errorMsg);
4637
+ const error = new Error(errorMsg);
4638
+ error.pluginId = pluginId;
4639
+ error.pluginName = pluginDef.name;
4640
+ error.missing = missing;
4641
+ error.incompatible = incompatible;
4642
+ error.docsUrl = pluginDef.docsUrl;
4643
+ throw error;
4351
4644
  }
4352
4645
  return { valid, missing, incompatible, messages };
4353
4646
  }
@@ -4364,7 +4657,6 @@ class ApiPlugin extends Plugin {
4364
4657
  port: options.port || 3e3,
4365
4658
  host: options.host || "0.0.0.0",
4366
4659
  verbose: options.verbose || false,
4367
- // API Documentation (supports both new and legacy formats)
4368
4660
  docs: {
4369
4661
  enabled: options.docs?.enabled !== false && options.docsEnabled !== false,
4370
4662
  // Enable by default
@@ -5743,8 +6035,6 @@ class MultiBackupDriver extends BaseBackupDriver {
5743
6035
  strategy: "all",
5744
6036
  // 'all', 'any', 'priority'
5745
6037
  concurrency: 3,
5746
- requireAll: true,
5747
- // For backward compatibility
5748
6038
  ...config
5749
6039
  });
5750
6040
  this.drivers = [];
@@ -6381,13 +6671,13 @@ class BackupPlugin extends Plugin {
6381
6671
  createdAt: now.toISOString().slice(0, 10)
6382
6672
  };
6383
6673
  const [ok] = await tryFn(
6384
- () => this.database.resource(this.config.backupMetadataResource).insert(metadata)
6674
+ () => this.database.resources[this.config.backupMetadataResource].insert(metadata)
6385
6675
  );
6386
6676
  return metadata;
6387
6677
  }
6388
6678
  async _updateBackupMetadata(backupId, updates) {
6389
6679
  const [ok] = await tryFn(
6390
- () => this.database.resource(this.config.backupMetadataResource).update(backupId, updates)
6680
+ () => this.database.resources[this.config.backupMetadataResource].update(backupId, updates)
6391
6681
  );
6392
6682
  }
6393
6683
  async _createBackupManifest(type, options) {
@@ -6422,7 +6712,7 @@ class BackupPlugin extends Plugin {
6422
6712
  let sinceTimestamp = null;
6423
6713
  if (type === "incremental") {
6424
6714
  const [lastBackupOk, , lastBackups] = await tryFn(
6425
- () => this.database.resource(this.config.backupMetadataResource).list({
6715
+ () => this.database.resources[this.config.backupMetadataResource].list({
6426
6716
  filter: {
6427
6717
  status: "completed",
6428
6718
  type: { $in: ["full", "incremental"] }
@@ -6730,7 +7020,7 @@ class BackupPlugin extends Plugin {
6730
7020
  try {
6731
7021
  const driverBackups = await this.driver.list(options);
6732
7022
  const [metaOk, , metadataRecords] = await tryFn(
6733
- () => this.database.resource(this.config.backupMetadataResource).list({
7023
+ () => this.database.resources[this.config.backupMetadataResource].list({
6734
7024
  limit: options.limit || 50,
6735
7025
  sort: { timestamp: -1 }
6736
7026
  })
@@ -6758,14 +7048,14 @@ class BackupPlugin extends Plugin {
6758
7048
  */
6759
7049
  async getBackupStatus(backupId) {
6760
7050
  const [ok, , backup] = await tryFn(
6761
- () => this.database.resource(this.config.backupMetadataResource).get(backupId)
7051
+ () => this.database.resources[this.config.backupMetadataResource].get(backupId)
6762
7052
  );
6763
7053
  return ok ? backup : null;
6764
7054
  }
6765
7055
  async _cleanupOldBackups() {
6766
7056
  try {
6767
7057
  const [listOk, , allBackups] = await tryFn(
6768
- () => this.database.resource(this.config.backupMetadataResource).list({
7058
+ () => this.database.resources[this.config.backupMetadataResource].list({
6769
7059
  filter: { status: "completed" },
6770
7060
  sort: { timestamp: -1 }
6771
7061
  })
@@ -6832,7 +7122,7 @@ class BackupPlugin extends Plugin {
6832
7122
  for (const backup of backupsToDelete) {
6833
7123
  try {
6834
7124
  await this.driver.delete(backup.id, backup.driverInfo);
6835
- await this.database.resource(this.config.backupMetadataResource).delete(backup.id);
7125
+ await this.database.resources[this.config.backupMetadataResource].delete(backup.id);
6836
7126
  if (this.config.verbose) {
6837
7127
  console.log(`[BackupPlugin] Deleted old backup: ${backup.id}`);
6838
7128
  }
@@ -6868,12 +7158,6 @@ class BackupPlugin extends Plugin {
6868
7158
  await this.driver.cleanup();
6869
7159
  }
6870
7160
  }
6871
- /**
6872
- * Cleanup plugin resources (alias for stop for backward compatibility)
6873
- */
6874
- async cleanup() {
6875
- await this.stop();
6876
- }
6877
7161
  }
6878
7162
 
6879
7163
  class CacheError extends S3dbError {
@@ -9769,9 +10053,6 @@ async function consolidateRecord(originalId, transactionResource, targetResource
9769
10053
  if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
9770
10054
  updateData.cohortMonth = txnWithCohorts.cohortMonth;
9771
10055
  }
9772
- if (txn.value === null || txn.value === void 0) {
9773
- updateData.value = 1;
9774
- }
9775
10056
  const [ok2, err2] = await tryFn(
9776
10057
  () => transactionResource.update(txn.id, updateData)
9777
10058
  );
@@ -11095,8 +11376,7 @@ async function completeFieldSetup(handler, database, config, plugin) {
11095
11376
  operation: "string|required",
11096
11377
  timestamp: "string|required",
11097
11378
  cohortDate: "string|required",
11098
- cohortHour: "string|optional",
11099
- // ✅ FIX BUG #2: Changed from required to optional for migration compatibility
11379
+ cohortHour: "string|required",
11100
11380
  cohortWeek: "string|optional",
11101
11381
  cohortMonth: "string|optional",
11102
11382
  source: "string|optional",
@@ -13430,7 +13710,7 @@ class RelationPlugin extends Plugin {
13430
13710
  * @private
13431
13711
  */
13432
13712
  async _setupResourceRelations(resourceName, relationsDef) {
13433
- const resource = this.database.resource(resourceName);
13713
+ const resource = this.database.resources[resourceName];
13434
13714
  if (!resource) {
13435
13715
  if (this.verbose) {
13436
13716
  console.warn(`[RelationPlugin] Resource "${resourceName}" not found, will setup when created`);
@@ -13541,7 +13821,7 @@ class RelationPlugin extends Plugin {
13541
13821
  for (const record of records) {
13542
13822
  const relatedData = record[relationName];
13543
13823
  if (relatedData) {
13544
- const relatedResource = this.database.resource(config.resource);
13824
+ const relatedResource = this.database.resources[config.resource];
13545
13825
  const relatedArray = Array.isArray(relatedData) ? relatedData : [relatedData];
13546
13826
  if (relatedArray.length > 0) {
13547
13827
  await this._eagerLoad(relatedArray, nestedIncludes, relatedResource);
@@ -13592,7 +13872,7 @@ class RelationPlugin extends Plugin {
13592
13872
  * @private
13593
13873
  */
13594
13874
  async _loadHasOne(records, relationName, config, sourceResource) {
13595
- const relatedResource = this.database.resource(config.resource);
13875
+ const relatedResource = this.database.resources[config.resource];
13596
13876
  if (!relatedResource) {
13597
13877
  throw new RelatedResourceNotFoundError(config.resource, {
13598
13878
  sourceResource: sourceResource.name,
@@ -13631,7 +13911,7 @@ class RelationPlugin extends Plugin {
13631
13911
  * @private
13632
13912
  */
13633
13913
  async _loadHasMany(records, relationName, config, sourceResource) {
13634
- const relatedResource = this.database.resource(config.resource);
13914
+ const relatedResource = this.database.resources[config.resource];
13635
13915
  if (!relatedResource) {
13636
13916
  throw new RelatedResourceNotFoundError(config.resource, {
13637
13917
  sourceResource: sourceResource.name,
@@ -13677,7 +13957,7 @@ class RelationPlugin extends Plugin {
13677
13957
  * @private
13678
13958
  */
13679
13959
  async _loadBelongsTo(records, relationName, config, sourceResource) {
13680
- const relatedResource = this.database.resource(config.resource);
13960
+ const relatedResource = this.database.resources[config.resource];
13681
13961
  if (!relatedResource) {
13682
13962
  throw new RelatedResourceNotFoundError(config.resource, {
13683
13963
  sourceResource: sourceResource.name,
@@ -13727,14 +14007,14 @@ class RelationPlugin extends Plugin {
13727
14007
  * @private
13728
14008
  */
13729
14009
  async _loadBelongsToMany(records, relationName, config, sourceResource) {
13730
- const relatedResource = this.database.resource(config.resource);
14010
+ const relatedResource = this.database.resources[config.resource];
13731
14011
  if (!relatedResource) {
13732
14012
  throw new RelatedResourceNotFoundError(config.resource, {
13733
14013
  sourceResource: sourceResource.name,
13734
14014
  relation: relationName
13735
14015
  });
13736
14016
  }
13737
- const junctionResource = this.database.resource(config.through);
14017
+ const junctionResource = this.database.resources[config.through];
13738
14018
  if (!junctionResource) {
13739
14019
  throw new JunctionTableNotFoundError(config.through, {
13740
14020
  sourceResource: sourceResource.name,
@@ -13918,7 +14198,7 @@ class RelationPlugin extends Plugin {
13918
14198
  */
13919
14199
  async _cascadeDelete(record, resource, relationName, config) {
13920
14200
  this.stats.cascadeOperations++;
13921
- const relatedResource = this.database.resource(config.resource);
14201
+ const relatedResource = this.database.resources[config.resource];
13922
14202
  if (!relatedResource) {
13923
14203
  throw new RelatedResourceNotFoundError(config.resource, {
13924
14204
  sourceResource: resource.name,
@@ -13926,7 +14206,7 @@ class RelationPlugin extends Plugin {
13926
14206
  });
13927
14207
  }
13928
14208
  const deletedRecords = [];
13929
- config.type === "belongsToMany" ? this.database.resource(config.through) : null;
14209
+ config.type === "belongsToMany" ? this.database.resources[config.through] : null;
13930
14210
  try {
13931
14211
  if (config.type === "hasMany") {
13932
14212
  let relatedRecords;
@@ -13977,7 +14257,7 @@ class RelationPlugin extends Plugin {
13977
14257
  await relatedResource.delete(relatedRecords[0].id);
13978
14258
  }
13979
14259
  } else if (config.type === "belongsToMany") {
13980
- const junctionResource2 = this.database.resource(config.through);
14260
+ const junctionResource2 = this.database.resources[config.through];
13981
14261
  if (junctionResource2) {
13982
14262
  let junctionRecords;
13983
14263
  const partitionName = this._findPartitionByField(junctionResource2, config.foreignKey);
@@ -14045,7 +14325,7 @@ class RelationPlugin extends Plugin {
14045
14325
  */
14046
14326
  async _cascadeUpdate(record, changes, resource, relationName, config) {
14047
14327
  this.stats.cascadeOperations++;
14048
- const relatedResource = this.database.resource(config.resource);
14328
+ const relatedResource = this.database.resources[config.resource];
14049
14329
  if (!relatedResource) {
14050
14330
  return;
14051
14331
  }
@@ -19330,12 +19610,42 @@ ${errorDetails}`,
19330
19610
  createdBy
19331
19611
  };
19332
19612
  this.hooks = {
19613
+ // Insert hooks
19333
19614
  beforeInsert: [],
19334
19615
  afterInsert: [],
19616
+ // Update hooks
19335
19617
  beforeUpdate: [],
19336
19618
  afterUpdate: [],
19619
+ // Delete hooks
19337
19620
  beforeDelete: [],
19338
- afterDelete: []
19621
+ afterDelete: [],
19622
+ // Get hooks
19623
+ beforeGet: [],
19624
+ afterGet: [],
19625
+ // List hooks
19626
+ beforeList: [],
19627
+ afterList: [],
19628
+ // Query hooks
19629
+ beforeQuery: [],
19630
+ afterQuery: [],
19631
+ // Patch hooks
19632
+ beforePatch: [],
19633
+ afterPatch: [],
19634
+ // Replace hooks
19635
+ beforeReplace: [],
19636
+ afterReplace: [],
19637
+ // Exists hooks
19638
+ beforeExists: [],
19639
+ afterExists: [],
19640
+ // Count hooks
19641
+ beforeCount: [],
19642
+ afterCount: [],
19643
+ // GetMany hooks
19644
+ beforeGetMany: [],
19645
+ afterGetMany: [],
19646
+ // DeleteMany hooks
19647
+ beforeDeleteMany: [],
19648
+ afterDeleteMany: []
19339
19649
  };
19340
19650
  this.attributes = attributes || {};
19341
19651
  this.map = config.map;
@@ -19398,19 +19708,6 @@ ${errorDetails}`,
19398
19708
  }
19399
19709
  return idSize;
19400
19710
  }
19401
- /**
19402
- * Get resource options (for backward compatibility with tests)
19403
- */
19404
- get options() {
19405
- return {
19406
- timestamps: this.config.timestamps,
19407
- partitions: this.config.partitions || {},
19408
- cache: this.config.cache,
19409
- autoDecrypt: this.config.autoDecrypt,
19410
- paranoid: this.config.paranoid,
19411
- allNestedObjectsOptional: this.config.allNestedObjectsOptional
19412
- };
19413
- }
19414
19711
  export() {
19415
19712
  const exported = this.schema.export();
19416
19713
  exported.behavior = this.behavior;
@@ -19537,19 +19834,71 @@ ${errorDetails}`,
19537
19834
  return data;
19538
19835
  });
19539
19836
  }
19540
- async validate(data) {
19837
+ /**
19838
+ * Validate data against resource schema without saving
19839
+ * @param {Object} data - Data to validate
19840
+ * @param {Object} options - Validation options
19841
+ * @param {boolean} options.throwOnError - Throw error if validation fails (default: false)
19842
+ * @param {boolean} options.includeId - Include ID validation (default: false)
19843
+ * @param {boolean} options.mutateOriginal - Allow mutation of original data (default: false)
19844
+ * @returns {Promise<{valid: boolean, isValid: boolean, errors: Array, data: Object, original: Object}>} Validation result
19845
+ * @example
19846
+ * // Validate before insert
19847
+ * const result = await resource.validate({
19848
+ * name: 'John Doe',
19849
+ * email: 'invalid-email' // Will fail email validation
19850
+ * });
19851
+ *
19852
+ * if (!result.valid) {
19853
+ * console.log('Validation errors:', result.errors);
19854
+ * // [{ field: 'email', message: '...', ... }]
19855
+ * }
19856
+ *
19857
+ * // Throw on error
19858
+ * try {
19859
+ * await resource.validate({ email: 'bad' }, { throwOnError: true });
19860
+ * } catch (err) {
19861
+ * console.log('Validation failed:', err.message);
19862
+ * }
19863
+ */
19864
+ async validate(data, options = {}) {
19865
+ const {
19866
+ throwOnError = false,
19867
+ includeId = false,
19868
+ mutateOriginal = false
19869
+ } = options;
19870
+ const dataToValidate = mutateOriginal ? data : cloneDeep(data);
19871
+ if (!includeId && dataToValidate.id) {
19872
+ delete dataToValidate.id;
19873
+ }
19541
19874
  const result = {
19542
19875
  original: cloneDeep(data),
19543
19876
  isValid: false,
19544
- errors: []
19877
+ errors: [],
19878
+ data: dataToValidate
19545
19879
  };
19546
- const check = await this.schema.validate(data, { mutateOriginal: false });
19547
- if (check === true) {
19548
- result.isValid = true;
19549
- } else {
19550
- result.errors = check;
19880
+ try {
19881
+ const check = await this.schema.validate(dataToValidate, { mutateOriginal });
19882
+ if (check === true) {
19883
+ result.isValid = true;
19884
+ } else {
19885
+ result.errors = Array.isArray(check) ? check : [check];
19886
+ result.isValid = false;
19887
+ if (throwOnError) {
19888
+ const error = new Error("Validation failed");
19889
+ error.validationErrors = result.errors;
19890
+ error.invalidData = data;
19891
+ throw error;
19892
+ }
19893
+ }
19894
+ } catch (err) {
19895
+ if (!throwOnError) {
19896
+ result.errors = [{ message: err.message, error: err }];
19897
+ result.isValid = false;
19898
+ } else {
19899
+ throw err;
19900
+ }
19551
19901
  }
19552
- result.data = data;
19553
19902
  return result;
19554
19903
  }
19555
19904
  /**
@@ -19814,12 +20163,12 @@ ${errorDetails}`,
19814
20163
  const exists = await this.exists(id$1);
19815
20164
  if (exists) throw new Error(`Resource with id '${id$1}' already exists`);
19816
20165
  this.getResourceKey(id$1 || "(auto)");
19817
- if (this.options.timestamps) {
20166
+ if (this.config.timestamps) {
19818
20167
  attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
19819
20168
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
19820
20169
  }
19821
20170
  const attributesWithDefaults = this.applyDefaults(attributes);
19822
- const completeData = { id: id$1, ...attributesWithDefaults };
20171
+ const completeData = id$1 !== void 0 ? { id: id$1, ...attributesWithDefaults } : { ...attributesWithDefaults };
19823
20172
  const preProcessedData = await this.executeHooks("beforeInsert", completeData);
19824
20173
  const extraProps = Object.keys(preProcessedData).filter(
19825
20174
  (k) => !(k in completeData) || preProcessedData[k] !== completeData[k]
@@ -19830,7 +20179,7 @@ ${errorDetails}`,
19830
20179
  errors,
19831
20180
  isValid,
19832
20181
  data: validated
19833
- } = await this.validate(preProcessedData);
20182
+ } = await this.validate(preProcessedData, { includeId: true });
19834
20183
  if (!isValid) {
19835
20184
  const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Insert failed";
19836
20185
  throw new InvalidResourceItem({
@@ -19934,6 +20283,7 @@ ${errorDetails}`,
19934
20283
  async get(id) {
19935
20284
  if (isObject(id)) throw new Error(`id cannot be an object`);
19936
20285
  if (isEmpty(id)) throw new Error("id cannot be empty");
20286
+ await this.executeHooks("beforeGet", { id });
19937
20287
  const key = this.getResourceKey(id);
19938
20288
  const [ok, err, request] = await tryFn(() => this.client.getObject(key));
19939
20289
  if (!ok) {
@@ -19982,17 +20332,67 @@ ${errorDetails}`,
19982
20332
  if (objectVersion !== this.version) {
19983
20333
  data = await this.applyVersionMapping(data, objectVersion, this.version);
19984
20334
  }
20335
+ data = await this.executeHooks("afterGet", data);
19985
20336
  this.emit("get", data);
19986
20337
  const value = data;
19987
20338
  return value;
19988
20339
  }
20340
+ /**
20341
+ * Retrieve a resource object by ID, or return null if not found
20342
+ * @param {string} id - Resource ID
20343
+ * @returns {Promise<Object|null>} The resource object or null if not found
20344
+ * @example
20345
+ * const user = await resource.getOrNull('user-123');
20346
+ * if (user) {
20347
+ * console.log('Found user:', user.name);
20348
+ * } else {
20349
+ * console.log('User not found');
20350
+ * }
20351
+ */
20352
+ async getOrNull(id) {
20353
+ const [ok, err, data] = await tryFn(() => this.get(id));
20354
+ if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
20355
+ return null;
20356
+ }
20357
+ if (!ok) {
20358
+ throw err;
20359
+ }
20360
+ return data;
20361
+ }
20362
+ /**
20363
+ * Retrieve a resource object by ID, or throw ResourceNotFoundError if not found
20364
+ * @param {string} id - Resource ID
20365
+ * @returns {Promise<Object>} The resource object
20366
+ * @throws {ResourceError} If resource does not exist
20367
+ * @example
20368
+ * // Throws error if user doesn't exist (no need for null check)
20369
+ * const user = await resource.getOrThrow('user-123');
20370
+ * console.log('User name:', user.name); // Safe to access
20371
+ */
20372
+ async getOrThrow(id) {
20373
+ const [ok, err, data] = await tryFn(() => this.get(id));
20374
+ if (!ok && err && (err.name === "NoSuchKey" || err.message?.includes("NoSuchKey"))) {
20375
+ throw new ResourceError(`Resource '${this.name}' with id '${id}' not found`, {
20376
+ resourceName: this.name,
20377
+ operation: "getOrThrow",
20378
+ id,
20379
+ code: "RESOURCE_NOT_FOUND"
20380
+ });
20381
+ }
20382
+ if (!ok) {
20383
+ throw err;
20384
+ }
20385
+ return data;
20386
+ }
19989
20387
  /**
19990
20388
  * Check if a resource exists by ID
19991
20389
  * @returns {Promise<boolean>} True if resource exists, false otherwise
19992
20390
  */
19993
20391
  async exists(id) {
20392
+ await this.executeHooks("beforeExists", { id });
19994
20393
  const key = this.getResourceKey(id);
19995
20394
  const [ok, err] = await tryFn(() => this.client.headObject(key));
20395
+ await this.executeHooks("afterExists", { id, exists: ok });
19996
20396
  return ok;
19997
20397
  }
19998
20398
  /**
@@ -20048,7 +20448,7 @@ ${errorDetails}`,
20048
20448
  }
20049
20449
  const preProcessedData = await this.executeHooks("beforeUpdate", cloneDeep(mergedData));
20050
20450
  const completeData = { ...originalData, ...preProcessedData, id };
20051
- const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
20451
+ const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
20052
20452
  if (!isValid) {
20053
20453
  throw new InvalidResourceItem({
20054
20454
  bucket: this.client.config.bucket,
@@ -20221,12 +20621,17 @@ ${errorDetails}`,
20221
20621
  if (!fields || typeof fields !== "object") {
20222
20622
  throw new Error("fields must be a non-empty object");
20223
20623
  }
20624
+ await this.executeHooks("beforePatch", { id, fields, options });
20224
20625
  const behavior = this.behavior;
20225
20626
  const hasNestedFields = Object.keys(fields).some((key) => key.includes("."));
20627
+ let result;
20226
20628
  if ((behavior === "enforce-limits" || behavior === "truncate-data") && !hasNestedFields) {
20227
- return await this._patchViaCopyObject(id, fields, options);
20629
+ result = await this._patchViaCopyObject(id, fields, options);
20630
+ } else {
20631
+ result = await this.update(id, fields, options);
20228
20632
  }
20229
- return await this.update(id, fields, options);
20633
+ const finalResult = await this.executeHooks("afterPatch", result);
20634
+ return finalResult;
20230
20635
  }
20231
20636
  /**
20232
20637
  * Internal helper: Optimized patch using HeadObject + CopyObject
@@ -20326,6 +20731,7 @@ ${errorDetails}`,
20326
20731
  if (!fullData || typeof fullData !== "object") {
20327
20732
  throw new Error("fullData must be a non-empty object");
20328
20733
  }
20734
+ await this.executeHooks("beforeReplace", { id, fullData, options });
20329
20735
  const { partition, partitionValues } = options;
20330
20736
  const dataClone = cloneDeep(fullData);
20331
20737
  const attributesWithDefaults = this.applyDefaults(dataClone);
@@ -20340,7 +20746,7 @@ ${errorDetails}`,
20340
20746
  errors,
20341
20747
  isValid,
20342
20748
  data: validated
20343
- } = await this.validate(completeData);
20749
+ } = await this.validate(completeData, { includeId: true });
20344
20750
  if (!isValid) {
20345
20751
  const errorMsg = errors && errors.length && errors[0].message ? errors[0].message : "Replace failed";
20346
20752
  throw new InvalidResourceItem({
@@ -20413,7 +20819,8 @@ ${errorDetails}`,
20413
20819
  await this.handlePartitionReferenceUpdates({}, replacedObject);
20414
20820
  }
20415
20821
  }
20416
- return replacedObject;
20822
+ const finalResult = await this.executeHooks("afterReplace", replacedObject);
20823
+ return finalResult;
20417
20824
  }
20418
20825
  /**
20419
20826
  * Update with conditional check (If-Match ETag)
@@ -20471,7 +20878,7 @@ ${errorDetails}`,
20471
20878
  }
20472
20879
  const preProcessedData = await this.executeHooks("beforeUpdate", cloneDeep(mergedData));
20473
20880
  const completeData = { ...originalData, ...preProcessedData, id };
20474
- const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
20881
+ const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
20475
20882
  if (!isValid) {
20476
20883
  return {
20477
20884
  success: false,
@@ -20692,6 +21099,7 @@ ${errorDetails}`,
20692
21099
  * });
20693
21100
  */
20694
21101
  async count({ partition = null, partitionValues = {} } = {}) {
21102
+ await this.executeHooks("beforeCount", { partition, partitionValues });
20695
21103
  let prefix;
20696
21104
  if (partition && Object.keys(partitionValues).length > 0) {
20697
21105
  const partitionDef = this.config.partitions[partition];
@@ -20716,6 +21124,7 @@ ${errorDetails}`,
20716
21124
  prefix = `resource=${this.name}/data`;
20717
21125
  }
20718
21126
  const count = await this.client.count({ prefix });
21127
+ await this.executeHooks("afterCount", { count, partition, partitionValues });
20719
21128
  this.emit("count", count);
20720
21129
  return count;
20721
21130
  }
@@ -20751,6 +21160,7 @@ ${errorDetails}`,
20751
21160
  * const results = await resource.deleteMany(deletedIds);
20752
21161
  */
20753
21162
  async deleteMany(ids) {
21163
+ await this.executeHooks("beforeDeleteMany", { ids });
20754
21164
  const packages = chunk(
20755
21165
  ids.map((id) => this.getResourceKey(id)),
20756
21166
  1e3
@@ -20772,6 +21182,7 @@ ${errorDetails}`,
20772
21182
  });
20773
21183
  return response;
20774
21184
  });
21185
+ await this.executeHooks("afterDeleteMany", { ids, results });
20775
21186
  this.emit("deleteMany", ids.length);
20776
21187
  return results;
20777
21188
  }
@@ -20893,6 +21304,7 @@ ${errorDetails}`,
20893
21304
  * });
20894
21305
  */
20895
21306
  async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
21307
+ await this.executeHooks("beforeList", { partition, partitionValues, limit, offset });
20896
21308
  const [ok, err, result] = await tryFn(async () => {
20897
21309
  if (!partition) {
20898
21310
  return await this.listMain({ limit, offset });
@@ -20902,7 +21314,8 @@ ${errorDetails}`,
20902
21314
  if (!ok) {
20903
21315
  return this.handleListError(err, { partition, partitionValues });
20904
21316
  }
20905
- return result;
21317
+ const finalResult = await this.executeHooks("afterList", result);
21318
+ return finalResult;
20906
21319
  }
20907
21320
  async listMain({ limit, offset = 0 }) {
20908
21321
  const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
@@ -21045,6 +21458,7 @@ ${errorDetails}`,
21045
21458
  * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
21046
21459
  */
21047
21460
  async getMany(ids) {
21461
+ await this.executeHooks("beforeGetMany", { ids });
21048
21462
  const { results, errors } = await PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
21049
21463
  this.emit("error", error, content);
21050
21464
  this.observers.map((x) => x.emit("error", this.name, error, content));
@@ -21065,8 +21479,9 @@ ${errorDetails}`,
21065
21479
  }
21066
21480
  throw err;
21067
21481
  });
21482
+ const finalResults = await this.executeHooks("afterGetMany", results);
21068
21483
  this.emit("getMany", ids.length);
21069
- return results;
21484
+ return finalResults;
21070
21485
  }
21071
21486
  /**
21072
21487
  * Get all resources (equivalent to list() without pagination)
@@ -21313,21 +21728,6 @@ ${errorDetails}`,
21313
21728
  * @returns {Object} Schema object for the version
21314
21729
  */
21315
21730
  async getSchemaForVersion(version) {
21316
- if (version === this.version) {
21317
- return this.schema;
21318
- }
21319
- const [ok, err, compatibleSchema] = await tryFn(() => Promise.resolve(new Schema({
21320
- name: this.name,
21321
- attributes: this.attributes,
21322
- passphrase: this.passphrase,
21323
- version,
21324
- options: {
21325
- ...this.config,
21326
- autoDecrypt: true,
21327
- autoEncrypt: true
21328
- }
21329
- })));
21330
- if (ok) return compatibleSchema;
21331
21731
  return this.schema;
21332
21732
  }
21333
21733
  /**
@@ -21423,6 +21823,7 @@ ${errorDetails}`,
21423
21823
  * );
21424
21824
  */
21425
21825
  async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
21826
+ await this.executeHooks("beforeQuery", { filter, limit, offset, partition, partitionValues });
21426
21827
  if (Object.keys(filter).length === 0) {
21427
21828
  return await this.list({ partition, partitionValues, limit, offset });
21428
21829
  }
@@ -21450,7 +21851,8 @@ ${errorDetails}`,
21450
21851
  break;
21451
21852
  }
21452
21853
  }
21453
- return results.slice(0, limit);
21854
+ const finalResults = results.slice(0, limit);
21855
+ return await this.executeHooks("afterQuery", finalResults);
21454
21856
  }
21455
21857
  /**
21456
21858
  * Handle partition reference updates with change detection
@@ -21530,7 +21932,7 @@ ${errorDetails}`,
21530
21932
  }
21531
21933
  }
21532
21934
  /**
21533
- * Update partition objects to keep them in sync (legacy method for backward compatibility)
21935
+ * Update partition objects to keep them in sync
21534
21936
  * @param {Object} data - Updated object data
21535
21937
  */
21536
21938
  async updatePartitionReferences(data) {
@@ -21916,7 +22318,32 @@ function validateResourceConfig(config) {
21916
22318
  if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) {
21917
22319
  errors.push("Resource 'hooks' must be an object");
21918
22320
  } else {
21919
- const validHookEvents = ["beforeInsert", "afterInsert", "beforeUpdate", "afterUpdate", "beforeDelete", "afterDelete"];
22321
+ const validHookEvents = [
22322
+ "beforeInsert",
22323
+ "afterInsert",
22324
+ "beforeUpdate",
22325
+ "afterUpdate",
22326
+ "beforeDelete",
22327
+ "afterDelete",
22328
+ "beforeGet",
22329
+ "afterGet",
22330
+ "beforeList",
22331
+ "afterList",
22332
+ "beforeQuery",
22333
+ "afterQuery",
22334
+ "beforeExists",
22335
+ "afterExists",
22336
+ "beforeCount",
22337
+ "afterCount",
22338
+ "beforePatch",
22339
+ "afterPatch",
22340
+ "beforeReplace",
22341
+ "afterReplace",
22342
+ "beforeGetMany",
22343
+ "afterGetMany",
22344
+ "beforeDeleteMany",
22345
+ "afterDeleteMany"
22346
+ ];
21920
22347
  for (const [event, hooksArr] of Object.entries(config.hooks)) {
21921
22348
  if (!validHookEvents.includes(event)) {
21922
22349
  errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(", ")}`);
@@ -21964,17 +22391,35 @@ class Database extends EventEmitter {
21964
22391
  this.id = idGenerator(7);
21965
22392
  this.version = "1";
21966
22393
  this.s3dbVersion = (() => {
21967
- const [ok, err, version] = tryFn(() => true ? "12.1.0" : "latest");
22394
+ const [ok, err, version] = tryFn(() => true ? "12.2.0" : "latest");
21968
22395
  return ok ? version : "latest";
21969
22396
  })();
21970
- this.resources = {};
22397
+ this._resourcesMap = {};
22398
+ this.resources = new Proxy(this._resourcesMap, {
22399
+ get: (target, prop) => {
22400
+ if (typeof prop === "symbol" || prop === "constructor" || prop === "toJSON") {
22401
+ return target[prop];
22402
+ }
22403
+ if (target[prop]) {
22404
+ return target[prop];
22405
+ }
22406
+ return void 0;
22407
+ },
22408
+ // Support Object.keys(), Object.entries(), etc.
22409
+ ownKeys: (target) => {
22410
+ return Object.keys(target);
22411
+ },
22412
+ getOwnPropertyDescriptor: (target, prop) => {
22413
+ return Object.getOwnPropertyDescriptor(target, prop);
22414
+ }
22415
+ });
21971
22416
  this.savedMetadata = null;
21972
22417
  this.options = options;
21973
22418
  this.verbose = options.verbose || false;
21974
22419
  this.parallelism = parseInt(options.parallelism + "") || 10;
21975
- this.plugins = options.plugins || [];
21976
- this.pluginRegistry = {};
21977
22420
  this.pluginList = options.plugins || [];
22421
+ this.pluginRegistry = {};
22422
+ this.plugins = this.pluginRegistry;
21978
22423
  this.cache = options.cache;
21979
22424
  this.passphrase = options.passphrase || "secret";
21980
22425
  this.versioningEnabled = options.versioningEnabled || false;
@@ -22080,7 +22525,7 @@ class Database extends EventEmitter {
22080
22525
  } else {
22081
22526
  restoredIdSize = versionData.idSize || 22;
22082
22527
  }
22083
- this.resources[name] = new Resource({
22528
+ this._resourcesMap[name] = new Resource({
22084
22529
  name,
22085
22530
  client: this.client,
22086
22531
  database: this,
@@ -22149,7 +22594,7 @@ class Database extends EventEmitter {
22149
22594
  }
22150
22595
  }
22151
22596
  for (const [name, savedResource] of Object.entries(savedMetadata.resources || {})) {
22152
- if (!this.resources[name]) {
22597
+ if (!this._resourcesMap[name]) {
22153
22598
  const currentVersion = savedResource.currentVersion || "v1";
22154
22599
  const versionData = savedResource.versions?.[currentVersion];
22155
22600
  changes.push({
@@ -22661,7 +23106,7 @@ class Database extends EventEmitter {
22661
23106
  * @returns {boolean} True if resource exists, false otherwise
22662
23107
  */
22663
23108
  resourceExists(name) {
22664
- return !!this.resources[name];
23109
+ return !!this._resourcesMap[name];
22665
23110
  }
22666
23111
  /**
22667
23112
  * Check if a resource exists with the same definition hash
@@ -22669,14 +23114,13 @@ class Database extends EventEmitter {
22669
23114
  * @param {string} config.name - Resource name
22670
23115
  * @param {Object} config.attributes - Resource attributes
22671
23116
  * @param {string} [config.behavior] - Resource behavior
22672
- * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
22673
23117
  * @returns {Object} Result with exists and hash information
22674
23118
  */
22675
- resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {}, options = {} }) {
22676
- if (!this.resources[name]) {
23119
+ resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", partitions = {} }) {
23120
+ if (!this._resourcesMap[name]) {
22677
23121
  return { exists: false, sameHash: false, hash: null };
22678
23122
  }
22679
- const existingResource = this.resources[name];
23123
+ const existingResource = this._resourcesMap[name];
22680
23124
  const existingHash = this.generateDefinitionHash(existingResource.export());
22681
23125
  const mockResource = new Resource({
22682
23126
  name,
@@ -22686,8 +23130,7 @@ class Database extends EventEmitter {
22686
23130
  client: this.client,
22687
23131
  version: existingResource.version,
22688
23132
  passphrase: this.passphrase,
22689
- versioningEnabled: this.versioningEnabled,
22690
- ...options
23133
+ versioningEnabled: this.versioningEnabled
22691
23134
  });
22692
23135
  const newHash = this.generateDefinitionHash(mockResource.export());
22693
23136
  return {
@@ -22715,12 +23158,49 @@ class Database extends EventEmitter {
22715
23158
  * @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
22716
23159
  * @returns {Promise<Resource>} The created or updated resource
22717
23160
  */
22718
- async createResource({ name, attributes, behavior = "user-managed", hooks, ...config }) {
22719
- if (this.resources[name]) {
22720
- const existingResource = this.resources[name];
23161
+ /**
23162
+ * Normalize partitions config from array or object format
23163
+ * @param {Array|Object} partitions - Partitions config
23164
+ * @param {Object} attributes - Resource attributes
23165
+ * @returns {Object} Normalized partitions object
23166
+ * @private
23167
+ */
23168
+ _normalizePartitions(partitions, attributes) {
23169
+ if (!Array.isArray(partitions)) {
23170
+ return partitions || {};
23171
+ }
23172
+ const normalized = {};
23173
+ for (const fieldName of partitions) {
23174
+ if (typeof fieldName !== "string") {
23175
+ throw new Error(`Partition field must be a string, got ${typeof fieldName}`);
23176
+ }
23177
+ if (!attributes[fieldName]) {
23178
+ throw new Error(`Partition field '${fieldName}' not found in attributes`);
23179
+ }
23180
+ const partitionName = `by${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
23181
+ const fieldDef = attributes[fieldName];
23182
+ let fieldType = "string";
23183
+ if (typeof fieldDef === "string") {
23184
+ fieldType = fieldDef.split("|")[0].trim();
23185
+ } else if (typeof fieldDef === "object" && fieldDef.type) {
23186
+ fieldType = fieldDef.type;
23187
+ }
23188
+ normalized[partitionName] = {
23189
+ fields: {
23190
+ [fieldName]: fieldType
23191
+ }
23192
+ };
23193
+ }
23194
+ return normalized;
23195
+ }
23196
+ async createResource({ name, attributes, behavior = "user-managed", hooks, middlewares, ...config }) {
23197
+ const normalizedPartitions = this._normalizePartitions(config.partitions, attributes);
23198
+ if (this._resourcesMap[name]) {
23199
+ const existingResource = this._resourcesMap[name];
22721
23200
  Object.assign(existingResource.config, {
22722
23201
  cache: this.cache,
22723
- ...config
23202
+ ...config,
23203
+ partitions: normalizedPartitions
22724
23204
  });
22725
23205
  if (behavior) {
22726
23206
  existingResource.behavior = behavior;
@@ -22738,6 +23218,9 @@ class Database extends EventEmitter {
22738
23218
  }
22739
23219
  }
22740
23220
  }
23221
+ if (middlewares) {
23222
+ this._applyMiddlewares(existingResource, middlewares);
23223
+ }
22741
23224
  const newHash = this.generateDefinitionHash(existingResource.export(), existingResource.behavior);
22742
23225
  const existingMetadata2 = this.savedMetadata?.resources?.[name];
22743
23226
  const currentVersion = existingMetadata2?.currentVersion || "v1";
@@ -22761,7 +23244,7 @@ class Database extends EventEmitter {
22761
23244
  observers: [this],
22762
23245
  cache: config.cache !== void 0 ? config.cache : this.cache,
22763
23246
  timestamps: config.timestamps !== void 0 ? config.timestamps : false,
22764
- partitions: config.partitions || {},
23247
+ partitions: normalizedPartitions,
22765
23248
  paranoid: config.paranoid !== void 0 ? config.paranoid : true,
22766
23249
  allNestedObjectsOptional: config.allNestedObjectsOptional !== void 0 ? config.allNestedObjectsOptional : true,
22767
23250
  autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
@@ -22777,16 +23260,96 @@ class Database extends EventEmitter {
22777
23260
  createdBy: config.createdBy || "user"
22778
23261
  });
22779
23262
  resource.database = this;
22780
- this.resources[name] = resource;
23263
+ this._resourcesMap[name] = resource;
23264
+ if (middlewares) {
23265
+ this._applyMiddlewares(resource, middlewares);
23266
+ }
22781
23267
  await this.uploadMetadataFile();
22782
23268
  this.emit("s3db.resourceCreated", name);
22783
23269
  return resource;
22784
23270
  }
22785
- resource(name) {
22786
- if (!this.resources[name]) {
22787
- return Promise.reject(`resource ${name} does not exist`);
23271
+ /**
23272
+ * Apply middlewares to a resource
23273
+ * @param {Resource} resource - Resource instance
23274
+ * @param {Array|Object} middlewares - Middlewares config
23275
+ * @private
23276
+ */
23277
+ _applyMiddlewares(resource, middlewares) {
23278
+ if (Array.isArray(middlewares)) {
23279
+ const methods = resource._middlewareMethods || [
23280
+ "get",
23281
+ "list",
23282
+ "listIds",
23283
+ "getAll",
23284
+ "count",
23285
+ "page",
23286
+ "insert",
23287
+ "update",
23288
+ "delete",
23289
+ "deleteMany",
23290
+ "exists",
23291
+ "getMany",
23292
+ "content",
23293
+ "hasContent",
23294
+ "query",
23295
+ "getFromPartition",
23296
+ "setContent",
23297
+ "deleteContent",
23298
+ "replace",
23299
+ "patch"
23300
+ ];
23301
+ for (const method of methods) {
23302
+ for (const middleware of middlewares) {
23303
+ if (typeof middleware === "function") {
23304
+ resource.useMiddleware(method, middleware);
23305
+ }
23306
+ }
23307
+ }
23308
+ return;
23309
+ }
23310
+ if (typeof middlewares === "object" && middlewares !== null) {
23311
+ for (const [method, fns] of Object.entries(middlewares)) {
23312
+ if (method === "*") {
23313
+ const methods = resource._middlewareMethods || [
23314
+ "get",
23315
+ "list",
23316
+ "listIds",
23317
+ "getAll",
23318
+ "count",
23319
+ "page",
23320
+ "insert",
23321
+ "update",
23322
+ "delete",
23323
+ "deleteMany",
23324
+ "exists",
23325
+ "getMany",
23326
+ "content",
23327
+ "hasContent",
23328
+ "query",
23329
+ "getFromPartition",
23330
+ "setContent",
23331
+ "deleteContent",
23332
+ "replace",
23333
+ "patch"
23334
+ ];
23335
+ const middlewareArray = Array.isArray(fns) ? fns : [fns];
23336
+ for (const targetMethod of methods) {
23337
+ for (const middleware of middlewareArray) {
23338
+ if (typeof middleware === "function") {
23339
+ resource.useMiddleware(targetMethod, middleware);
23340
+ }
23341
+ }
23342
+ }
23343
+ } else {
23344
+ const middlewareArray = Array.isArray(fns) ? fns : [fns];
23345
+ for (const middleware of middlewareArray) {
23346
+ if (typeof middleware === "function") {
23347
+ resource.useMiddleware(method, middleware);
23348
+ }
23349
+ }
23350
+ }
23351
+ }
22788
23352
  }
22789
- return this.resources[name];
22790
23353
  }
22791
23354
  /**
22792
23355
  * List all resource names
@@ -22801,14 +23364,14 @@ class Database extends EventEmitter {
22801
23364
  * @returns {Resource} Resource instance
22802
23365
  */
22803
23366
  async getResource(name) {
22804
- if (!this.resources[name]) {
23367
+ if (!this._resourcesMap[name]) {
22805
23368
  throw new ResourceNotFound({
22806
23369
  bucket: this.client.config.bucket,
22807
23370
  resourceName: name,
22808
23371
  id: name
22809
23372
  });
22810
23373
  }
22811
- return this.resources[name];
23374
+ return this._resourcesMap[name];
22812
23375
  }
22813
23376
  /**
22814
23377
  * Get database configuration
@@ -22861,7 +23424,7 @@ class Database extends EventEmitter {
22861
23424
  }
22862
23425
  });
22863
23426
  }
22864
- Object.keys(this.resources).forEach((k) => delete this.resources[k]);
23427
+ Object.keys(this.resources).forEach((k) => delete this._resourcesMap[k]);
22865
23428
  }
22866
23429
  if (this.client && typeof this.client.removeAllListeners === "function") {
22867
23430
  this.client.removeAllListeners();
@@ -24605,14 +25168,6 @@ class ReplicatorPlugin extends Plugin {
24605
25168
  }
24606
25169
  async start() {
24607
25170
  }
24608
- async stop() {
24609
- for (const replicator of this.replicators || []) {
24610
- if (replicator && typeof replicator.cleanup === "function") {
24611
- await replicator.cleanup();
24612
- }
24613
- }
24614
- this.removeDatabaseHooks();
24615
- }
24616
25171
  installDatabaseHooks() {
24617
25172
  this._afterCreateResourceHook = (resource) => {
24618
25173
  if (resource.name !== (this.config.replicatorLogResource || "plg_replicator_logs")) {
@@ -24942,20 +25497,20 @@ class ReplicatorPlugin extends Plugin {
24942
25497
  }
24943
25498
  this.emit("replicator.sync.completed", { replicatorId, stats: this.stats });
24944
25499
  }
24945
- async cleanup() {
25500
+ async stop() {
24946
25501
  const [ok, error] = await tryFn(async () => {
24947
25502
  if (this.replicators && this.replicators.length > 0) {
24948
25503
  const cleanupPromises = this.replicators.map(async (replicator) => {
24949
25504
  const [replicatorOk, replicatorError] = await tryFn(async () => {
24950
- if (replicator && typeof replicator.cleanup === "function") {
24951
- await replicator.cleanup();
25505
+ if (replicator && typeof replicator.stop === "function") {
25506
+ await replicator.stop();
24952
25507
  }
24953
25508
  });
24954
25509
  if (!replicatorOk) {
24955
25510
  if (this.config.verbose) {
24956
- console.warn(`[ReplicatorPlugin] Failed to cleanup replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
25511
+ console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
24957
25512
  }
24958
- this.emit("replicator_cleanup_error", {
25513
+ this.emit("replicator_stop_error", {
24959
25514
  replicator: replicator.name || replicator.id || "unknown",
24960
25515
  driver: replicator.driver || "unknown",
24961
25516
  error: replicatorError.message
@@ -24964,6 +25519,7 @@ class ReplicatorPlugin extends Plugin {
24964
25519
  });
24965
25520
  await Promise.allSettled(cleanupPromises);
24966
25521
  }
25522
+ this.removeDatabaseHooks();
24967
25523
  if (this.database && this.database.resources) {
24968
25524
  for (const resourceName of this.eventListenersInstalled) {
24969
25525
  const resource = this.database.resources[resourceName];
@@ -24983,9 +25539,9 @@ class ReplicatorPlugin extends Plugin {
24983
25539
  });
24984
25540
  if (!ok) {
24985
25541
  if (this.config.verbose) {
24986
- console.warn(`[ReplicatorPlugin] Failed to cleanup plugin: ${error.message}`);
25542
+ console.warn(`[ReplicatorPlugin] Failed to stop plugin: ${error.message}`);
24987
25543
  }
24988
- this.emit("replicator_plugin_cleanup_error", {
25544
+ this.emit("replicator_plugin_stop_error", {
24989
25545
  error: error.message
24990
25546
  });
24991
25547
  }
@@ -25805,7 +26361,7 @@ class SchedulerPlugin extends Plugin {
25805
26361
  }
25806
26362
  async _persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, error, retryCount) {
25807
26363
  const [ok, err] = await tryFn(
25808
- () => this.database.resource(this.config.jobHistoryResource).insert({
26364
+ () => this.database.resources[this.config.jobHistoryResource].insert({
25809
26365
  id: executionId,
25810
26366
  jobName,
25811
26367
  status,
@@ -25946,7 +26502,7 @@ class SchedulerPlugin extends Plugin {
25946
26502
  queryParams.status = status;
25947
26503
  }
25948
26504
  const [ok, err, history] = await tryFn(
25949
- () => this.database.resource(this.config.jobHistoryResource).query(queryParams)
26505
+ () => this.database.resources[this.config.jobHistoryResource].query(queryParams)
25950
26506
  );
25951
26507
  if (!ok) {
25952
26508
  if (this.config.verbose) {
@@ -26085,9 +26641,6 @@ class SchedulerPlugin extends Plugin {
26085
26641
  if (this._isTestEnvironment()) {
26086
26642
  this.activeJobs.clear();
26087
26643
  }
26088
- }
26089
- async cleanup() {
26090
- await this.stop();
26091
26644
  this.jobs.clear();
26092
26645
  this.statistics.clear();
26093
26646
  this.activeJobs.clear();
@@ -26336,7 +26889,7 @@ class StateMachinePlugin extends Plugin {
26336
26889
  let lastLogErr;
26337
26890
  for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
26338
26891
  const [ok, err] = await tryFn(
26339
- () => this.database.resource(this.config.transitionLogResource).insert({
26892
+ () => this.database.resources[this.config.transitionLogResource].insert({
26340
26893
  id: transitionId,
26341
26894
  machineId,
26342
26895
  entityId,
@@ -26372,11 +26925,11 @@ class StateMachinePlugin extends Plugin {
26372
26925
  updatedAt: now
26373
26926
  };
26374
26927
  const [updateOk] = await tryFn(
26375
- () => this.database.resource(this.config.stateResource).update(stateId, stateData)
26928
+ () => this.database.resources[this.config.stateResource].update(stateId, stateData)
26376
26929
  );
26377
26930
  if (!updateOk) {
26378
26931
  const [insertOk, insertErr] = await tryFn(
26379
- () => this.database.resource(this.config.stateResource).insert({ id: stateId, ...stateData })
26932
+ () => this.database.resources[this.config.stateResource].insert({ id: stateId, ...stateData })
26380
26933
  );
26381
26934
  if (!insertOk && this.config.verbose) {
26382
26935
  console.warn(`[StateMachinePlugin] Failed to upsert state:`, insertErr.message);
@@ -26439,7 +26992,7 @@ class StateMachinePlugin extends Plugin {
26439
26992
  if (this.config.persistTransitions) {
26440
26993
  const stateId = `${machineId}_${entityId}`;
26441
26994
  const [ok, err, stateRecord] = await tryFn(
26442
- () => this.database.resource(this.config.stateResource).get(stateId)
26995
+ () => this.database.resources[this.config.stateResource].get(stateId)
26443
26996
  );
26444
26997
  if (ok && stateRecord) {
26445
26998
  machine.currentStates.set(entityId, stateRecord.currentState);
@@ -26482,7 +27035,7 @@ class StateMachinePlugin extends Plugin {
26482
27035
  }
26483
27036
  const { limit = 50, offset = 0 } = options;
26484
27037
  const [ok, err, transitions] = await tryFn(
26485
- () => this.database.resource(this.config.transitionLogResource).query({
27038
+ () => this.database.resources[this.config.transitionLogResource].query({
26486
27039
  machineId,
26487
27040
  entityId
26488
27041
  }, {
@@ -26524,7 +27077,7 @@ class StateMachinePlugin extends Plugin {
26524
27077
  const now = (/* @__PURE__ */ new Date()).toISOString();
26525
27078
  const stateId = `${machineId}_${entityId}`;
26526
27079
  const [ok, err] = await tryFn(
26527
- () => this.database.resource(this.config.stateResource).insert({
27080
+ () => this.database.resources[this.config.stateResource].insert({
26528
27081
  id: stateId,
26529
27082
  machineId,
26530
27083
  entityId,
@@ -26613,9 +27166,6 @@ class StateMachinePlugin extends Plugin {
26613
27166
  }
26614
27167
  async stop() {
26615
27168
  this.machines.clear();
26616
- }
26617
- async cleanup() {
26618
- await this.stop();
26619
27169
  this.removeAllListeners();
26620
27170
  }
26621
27171
  }
@@ -26630,7 +27180,7 @@ class TfStateError extends Error {
26630
27180
  }
26631
27181
  class InvalidStateFileError extends TfStateError {
26632
27182
  constructor(filePath, reason, context = {}) {
26633
- super(`Invalid Terraform state file "${filePath}": ${reason}`, context);
27183
+ super(`Invalid Tfstate file "${filePath}": ${reason}`, context);
26634
27184
  this.name = "InvalidStateFileError";
26635
27185
  this.filePath = filePath;
26636
27186
  this.reason = reason;
@@ -26639,7 +27189,7 @@ class InvalidStateFileError extends TfStateError {
26639
27189
  class UnsupportedStateVersionError extends TfStateError {
26640
27190
  constructor(version, supportedVersions, context = {}) {
26641
27191
  super(
26642
- `Terraform state version ${version} is not supported. Supported versions: ${supportedVersions.join(", ")}`,
27192
+ `Tfstate version ${version} is not supported. Supported versions: ${supportedVersions.join(", ")}`,
26643
27193
  context
26644
27194
  );
26645
27195
  this.name = "UnsupportedStateVersionError";
@@ -26649,7 +27199,7 @@ class UnsupportedStateVersionError extends TfStateError {
26649
27199
  }
26650
27200
  class StateFileNotFoundError extends TfStateError {
26651
27201
  constructor(filePath, context = {}) {
26652
- super(`Terraform state file not found: ${filePath}`, context);
27202
+ super(`Tfstate file not found: ${filePath}`, context);
26653
27203
  this.name = "StateFileNotFoundError";
26654
27204
  this.filePath = filePath;
26655
27205
  }
@@ -34872,42 +35422,23 @@ class FilesystemTfStateDriver extends TfStateDriver {
34872
35422
  class TfStatePlugin extends Plugin {
34873
35423
  constructor(config = {}) {
34874
35424
  super(config);
34875
- const isNewFormat = config.driver !== void 0;
34876
- if (isNewFormat) {
34877
- this.driverType = config.driver || "s3";
34878
- this.driverConfig = config.config || {};
34879
- const resources = config.resources || {};
34880
- this.resourceName = resources.resources || "plg_tfstate_resources";
34881
- this.stateFilesName = resources.stateFiles || "plg_tfstate_state_files";
34882
- this.diffsName = resources.diffs || "plg_tfstate_state_diffs";
34883
- const monitor = config.monitor || {};
34884
- this.monitorEnabled = monitor.enabled || false;
34885
- this.monitorCron = monitor.cron || "*/5 * * * *";
34886
- const diffs = config.diffs || {};
34887
- this.trackDiffs = diffs.enabled !== void 0 ? diffs.enabled : true;
34888
- this.diffsLookback = diffs.lookback || 10;
34889
- this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
34890
- this.autoSync = false;
34891
- this.watchPaths = [];
34892
- this.filters = config.filters || {};
34893
- this.verbose = config.verbose || false;
34894
- } else {
34895
- this.driverType = null;
34896
- this.driverConfig = {};
34897
- this.resourceName = config.resourceName || "plg_tfstate_resources";
34898
- this.stateFilesName = config.stateFilesName || "plg_tfstate_state_files";
34899
- this.diffsName = config.diffsName || config.stateHistoryName || "plg_tfstate_state_diffs";
34900
- this.stateHistoryName = this.diffsName;
34901
- this.autoSync = config.autoSync || false;
34902
- this.watchPaths = Array.isArray(config.watchPaths) ? config.watchPaths : [];
34903
- this.filters = config.filters || {};
34904
- this.trackDiffs = config.trackDiffs !== void 0 ? config.trackDiffs : true;
34905
- this.diffsLookback = 10;
34906
- this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
34907
- this.verbose = config.verbose || false;
34908
- this.monitorEnabled = false;
34909
- this.monitorCron = "*/5 * * * *";
34910
- }
35425
+ this.driverType = config.driver || null;
35426
+ this.driverConfig = config.config || {};
35427
+ const resources = config.resources || {};
35428
+ this.resourceName = resources.resources || config.resourceName || "plg_tfstate_resources";
35429
+ this.stateFilesName = resources.stateFiles || config.stateFilesName || "plg_tfstate_state_files";
35430
+ this.diffsName = resources.diffs || config.diffsName || "plg_tfstate_state_diffs";
35431
+ const monitor = config.monitor || {};
35432
+ this.monitorEnabled = monitor.enabled || false;
35433
+ this.monitorCron = monitor.cron || "*/5 * * * *";
35434
+ const diffs = config.diffs || {};
35435
+ this.trackDiffs = diffs.enabled !== void 0 ? diffs.enabled : config.trackDiffs !== void 0 ? config.trackDiffs : true;
35436
+ this.diffsLookback = diffs.lookback || 10;
35437
+ this.asyncPartitions = config.asyncPartitions !== void 0 ? config.asyncPartitions : true;
35438
+ this.autoSync = config.autoSync || false;
35439
+ this.watchPaths = config.watchPaths || [];
35440
+ this.filters = config.filters || {};
35441
+ this.verbose = config.verbose || false;
34911
35442
  this.supportedVersions = [3, 4];
34912
35443
  this.driver = null;
34913
35444
  this.resource = null;
@@ -34957,7 +35488,7 @@ class TfStatePlugin extends Plugin {
34957
35488
  name: this.lineagesName,
34958
35489
  attributes: {
34959
35490
  id: "string|required",
34960
- // = lineage UUID from Terraform state
35491
+ // = lineage UUID from Tfstate
34961
35492
  latestSerial: "number",
34962
35493
  // Track latest for quick access
34963
35494
  latestStateId: "string",
@@ -35670,7 +36201,7 @@ class TfStatePlugin extends Plugin {
35670
36201
  return result;
35671
36202
  }
35672
36203
  /**
35673
- * Read and parse Terraform state file
36204
+ * Read and parse Tfstate file
35674
36205
  * @private
35675
36206
  */
35676
36207
  async _readStateFile(filePath) {
@@ -35707,7 +36238,7 @@ class TfStatePlugin extends Plugin {
35707
36238
  }
35708
36239
  }
35709
36240
  /**
35710
- * Validate Terraform state version
36241
+ * Validate Tfstate version
35711
36242
  * @private
35712
36243
  */
35713
36244
  _validateStateVersion(state) {
@@ -35720,7 +36251,7 @@ class TfStatePlugin extends Plugin {
35720
36251
  }
35721
36252
  }
35722
36253
  /**
35723
- * Extract resources from Terraform state
36254
+ * Extract resources from Tfstate
35724
36255
  * @private
35725
36256
  */
35726
36257
  async _extractResources(state, filePath, stateFileId, lineageId) {
@@ -36287,14 +36818,14 @@ class TfStatePlugin extends Plugin {
36287
36818
  }
36288
36819
  }
36289
36820
  /**
36290
- * Export resources to Terraform state format
36821
+ * Export resources to Tfstate format
36291
36822
  * @param {Object} options - Export options
36292
36823
  * @param {number} options.serial - Specific serial to export (default: latest)
36293
36824
  * @param {string[]} options.resourceTypes - Filter by resource types
36294
36825
  * @param {string} options.terraformVersion - Terraform version for output (default: '1.5.0')
36295
36826
  * @param {string} options.lineage - State lineage (default: auto-generated)
36296
36827
  * @param {Object} options.outputs - Terraform outputs to include
36297
- * @returns {Promise<Object>} Terraform state object
36828
+ * @returns {Promise<Object>} Tfstate object
36298
36829
  *
36299
36830
  * @example
36300
36831
  * // Export latest state
@@ -37644,6 +38175,533 @@ class VectorPlugin extends Plugin {
37644
38175
  }
37645
38176
  }
37646
38177
 
38178
+ function mapFieldTypeToTypeScript(fieldType) {
38179
+ const baseType = fieldType.split("|")[0].trim();
38180
+ const typeMap = {
38181
+ "string": "string",
38182
+ "number": "number",
38183
+ "integer": "number",
38184
+ "boolean": "boolean",
38185
+ "array": "any[]",
38186
+ "object": "Record<string, any>",
38187
+ "json": "Record<string, any>",
38188
+ "secret": "string",
38189
+ "email": "string",
38190
+ "url": "string",
38191
+ "date": "string",
38192
+ // ISO date string
38193
+ "datetime": "string",
38194
+ // ISO datetime string
38195
+ "ip4": "string",
38196
+ "ip6": "string"
38197
+ };
38198
+ if (baseType.startsWith("embedding:")) {
38199
+ const dimensions = parseInt(baseType.split(":")[1]);
38200
+ return `number[] /* ${dimensions} dimensions */`;
38201
+ }
38202
+ return typeMap[baseType] || "any";
38203
+ }
38204
+ function isFieldRequired(fieldDef) {
38205
+ if (typeof fieldDef === "string") {
38206
+ return fieldDef.includes("|required");
38207
+ }
38208
+ if (typeof fieldDef === "object" && fieldDef.required) {
38209
+ return true;
38210
+ }
38211
+ return false;
38212
+ }
38213
+ function generateResourceInterface(resourceName, attributes, timestamps = false) {
38214
+ const interfaceName = toPascalCase(resourceName);
38215
+ const lines = [];
38216
+ lines.push(`export interface ${interfaceName} {`);
38217
+ lines.push(` /** Resource ID (auto-generated) */`);
38218
+ lines.push(` id: string;`);
38219
+ lines.push("");
38220
+ for (const [fieldName, fieldDef] of Object.entries(attributes)) {
38221
+ const required = isFieldRequired(fieldDef);
38222
+ const optional = required ? "" : "?";
38223
+ let tsType;
38224
+ if (typeof fieldDef === "string") {
38225
+ tsType = mapFieldTypeToTypeScript(fieldDef);
38226
+ } else if (typeof fieldDef === "object" && fieldDef.type) {
38227
+ tsType = mapFieldTypeToTypeScript(fieldDef.type);
38228
+ if (fieldDef.type === "object" && fieldDef.props) {
38229
+ tsType = "{\n";
38230
+ for (const [propName, propDef] of Object.entries(fieldDef.props)) {
38231
+ const propType = typeof propDef === "string" ? mapFieldTypeToTypeScript(propDef) : mapFieldTypeToTypeScript(propDef.type);
38232
+ const propRequired = isFieldRequired(propDef);
38233
+ tsType += ` ${propName}${propRequired ? "" : "?"}: ${propType};
38234
+ `;
38235
+ }
38236
+ tsType += " }";
38237
+ }
38238
+ if (fieldDef.type === "array" && fieldDef.items) {
38239
+ const itemType = mapFieldTypeToTypeScript(fieldDef.items);
38240
+ tsType = `Array<${itemType}>`;
38241
+ }
38242
+ } else {
38243
+ tsType = "any";
38244
+ }
38245
+ if (fieldDef.description) {
38246
+ lines.push(` /** ${fieldDef.description} */`);
38247
+ }
38248
+ lines.push(` ${fieldName}${optional}: ${tsType};`);
38249
+ }
38250
+ if (timestamps) {
38251
+ lines.push("");
38252
+ lines.push(` /** Creation timestamp (ISO 8601) */`);
38253
+ lines.push(` createdAt: string;`);
38254
+ lines.push(` /** Last update timestamp (ISO 8601) */`);
38255
+ lines.push(` updatedAt: string;`);
38256
+ }
38257
+ lines.push("}");
38258
+ lines.push("");
38259
+ return lines.join("\n");
38260
+ }
38261
+ function toPascalCase(str) {
38262
+ return str.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
38263
+ }
38264
+ async function generateTypes(database, options = {}) {
38265
+ const {
38266
+ outputPath = "./types/database.d.ts",
38267
+ moduleName = "s3db.js",
38268
+ includeResource = true
38269
+ } = options;
38270
+ const lines = [];
38271
+ lines.push("/**");
38272
+ lines.push(" * Auto-generated TypeScript definitions for s3db.js resources");
38273
+ lines.push(" * Generated at: " + (/* @__PURE__ */ new Date()).toISOString());
38274
+ lines.push(" * DO NOT EDIT - This file is auto-generated");
38275
+ lines.push(" */");
38276
+ lines.push("");
38277
+ if (includeResource) {
38278
+ lines.push(`import { Resource, Database } from '${moduleName}';`);
38279
+ lines.push("");
38280
+ }
38281
+ const resourceInterfaces = [];
38282
+ for (const [name, resource] of Object.entries(database.resources)) {
38283
+ const attributes = resource.config?.attributes || resource.attributes || {};
38284
+ const timestamps = resource.config?.timestamps || false;
38285
+ const interfaceDef = generateResourceInterface(name, attributes, timestamps);
38286
+ lines.push(interfaceDef);
38287
+ resourceInterfaces.push({
38288
+ name,
38289
+ interfaceName: toPascalCase(name),
38290
+ resource
38291
+ });
38292
+ }
38293
+ lines.push("/**");
38294
+ lines.push(" * Typed resource map for property access");
38295
+ lines.push(" * @example");
38296
+ lines.push(" * const users = db.resources.users; // Type-safe!");
38297
+ lines.push(' * const user = await users.get("id"); // Autocomplete works!');
38298
+ lines.push(" */");
38299
+ lines.push("export interface ResourceMap {");
38300
+ for (const { name, interfaceName } of resourceInterfaces) {
38301
+ lines.push(` /** ${interfaceName} resource */`);
38302
+ if (includeResource) {
38303
+ lines.push(` ${name}: Resource<${interfaceName}>;`);
38304
+ } else {
38305
+ lines.push(` ${name}: any;`);
38306
+ }
38307
+ }
38308
+ lines.push("}");
38309
+ lines.push("");
38310
+ if (includeResource) {
38311
+ lines.push("/**");
38312
+ lines.push(" * Extended Database class with typed resources");
38313
+ lines.push(" */");
38314
+ lines.push("declare module 's3db.js' {");
38315
+ lines.push(" interface Database {");
38316
+ lines.push(" resources: ResourceMap;");
38317
+ lines.push(" }");
38318
+ lines.push("");
38319
+ lines.push(" interface Resource<T = any> {");
38320
+ lines.push(" get(id: string): Promise<T>;");
38321
+ lines.push(" getOrNull(id: string): Promise<T | null>;");
38322
+ lines.push(" getOrThrow(id: string): Promise<T>;");
38323
+ lines.push(" insert(data: Partial<T>): Promise<T>;");
38324
+ lines.push(" update(id: string, data: Partial<T>): Promise<T>;");
38325
+ lines.push(" patch(id: string, data: Partial<T>): Promise<T>;");
38326
+ lines.push(" replace(id: string, data: Partial<T>): Promise<T>;");
38327
+ lines.push(" delete(id: string): Promise<void>;");
38328
+ lines.push(" list(options?: any): Promise<T[]>;");
38329
+ lines.push(" query(filters: Partial<T>, options?: any): Promise<T[]>;");
38330
+ lines.push(" validate(data: Partial<T>, options?: any): Promise<{ valid: boolean; errors: any[]; data: T | null }>;");
38331
+ lines.push(" }");
38332
+ lines.push("}");
38333
+ }
38334
+ const content = lines.join("\n");
38335
+ if (outputPath) {
38336
+ await mkdir(dirname(outputPath), { recursive: true });
38337
+ await writeFile(outputPath, content, "utf-8");
38338
+ }
38339
+ return content;
38340
+ }
38341
+ async function printTypes(database, options = {}) {
38342
+ const types = await generateTypes(database, { ...options, outputPath: null });
38343
+ console.log(types);
38344
+ return types;
38345
+ }
38346
+
38347
+ class Factory {
38348
+ /**
38349
+ * Global sequence counter
38350
+ * @private
38351
+ */
38352
+ static _sequences = /* @__PURE__ */ new Map();
38353
+ /**
38354
+ * Registered factories
38355
+ * @private
38356
+ */
38357
+ static _factories = /* @__PURE__ */ new Map();
38358
+ /**
38359
+ * Database instance (set globally)
38360
+ * @private
38361
+ */
38362
+ static _database = null;
38363
+ /**
38364
+ * Create a new factory definition
38365
+ * @param {string} resourceName - Resource name
38366
+ * @param {Object|Function} definition - Field definitions or function
38367
+ * @param {Object} options - Factory options
38368
+ * @returns {Factory} Factory instance
38369
+ */
38370
+ static define(resourceName, definition, options = {}) {
38371
+ const factory = new Factory(resourceName, definition, options);
38372
+ Factory._factories.set(resourceName, factory);
38373
+ return factory;
38374
+ }
38375
+ /**
38376
+ * Set global database instance
38377
+ * @param {Database} database - s3db.js Database instance
38378
+ */
38379
+ static setDatabase(database) {
38380
+ Factory._database = database;
38381
+ }
38382
+ /**
38383
+ * Get factory by resource name
38384
+ * @param {string} resourceName - Resource name
38385
+ * @returns {Factory} Factory instance
38386
+ */
38387
+ static get(resourceName) {
38388
+ return Factory._factories.get(resourceName);
38389
+ }
38390
+ /**
38391
+ * Reset all sequences
38392
+ */
38393
+ static resetSequences() {
38394
+ Factory._sequences.clear();
38395
+ }
38396
+ /**
38397
+ * Reset all factories
38398
+ */
38399
+ static reset() {
38400
+ Factory._sequences.clear();
38401
+ Factory._factories.clear();
38402
+ Factory._database = null;
38403
+ }
38404
+ /**
38405
+ * Constructor
38406
+ * @param {string} resourceName - Resource name
38407
+ * @param {Object|Function} definition - Field definitions
38408
+ * @param {Object} options - Factory options
38409
+ */
38410
+ constructor(resourceName, definition, options = {}) {
38411
+ this.resourceName = resourceName;
38412
+ this.definition = definition;
38413
+ this.options = options;
38414
+ this.traits = /* @__PURE__ */ new Map();
38415
+ this.afterCreateCallbacks = [];
38416
+ this.beforeCreateCallbacks = [];
38417
+ }
38418
+ /**
38419
+ * Get next sequence number
38420
+ * @param {string} name - Sequence name (default: factory name)
38421
+ * @returns {number} Next sequence number
38422
+ */
38423
+ sequence(name = this.resourceName) {
38424
+ const current = Factory._sequences.get(name) || 0;
38425
+ const next = current + 1;
38426
+ Factory._sequences.set(name, next);
38427
+ return next;
38428
+ }
38429
+ /**
38430
+ * Define a trait (state variation)
38431
+ * @param {string} name - Trait name
38432
+ * @param {Object|Function} attributes - Trait attributes
38433
+ * @returns {Factory} This factory (for chaining)
38434
+ */
38435
+ trait(name, attributes) {
38436
+ this.traits.set(name, attributes);
38437
+ return this;
38438
+ }
38439
+ /**
38440
+ * Register after create callback
38441
+ * @param {Function} callback - Callback function
38442
+ * @returns {Factory} This factory (for chaining)
38443
+ */
38444
+ afterCreate(callback) {
38445
+ this.afterCreateCallbacks.push(callback);
38446
+ return this;
38447
+ }
38448
+ /**
38449
+ * Register before create callback
38450
+ * @param {Function} callback - Callback function
38451
+ * @returns {Factory} This factory (for chaining)
38452
+ */
38453
+ beforeCreate(callback) {
38454
+ this.beforeCreateCallbacks.push(callback);
38455
+ return this;
38456
+ }
38457
+ /**
38458
+ * Build attributes without creating in database
38459
+ * @param {Object} overrides - Override attributes
38460
+ * @param {Object} options - Build options
38461
+ * @returns {Promise<Object>} Built attributes
38462
+ */
38463
+ async build(overrides = {}, options = {}) {
38464
+ const { traits = [] } = options;
38465
+ const seq = this.sequence();
38466
+ let attributes = typeof this.definition === "function" ? await this.definition({ seq, factory: this }) : { ...this.definition };
38467
+ for (const traitName of traits) {
38468
+ const trait = this.traits.get(traitName);
38469
+ if (!trait) {
38470
+ throw new Error(`Trait '${traitName}' not found in factory '${this.resourceName}'`);
38471
+ }
38472
+ const traitAttrs = typeof trait === "function" ? await trait({ seq, factory: this }) : trait;
38473
+ attributes = { ...attributes, ...traitAttrs };
38474
+ }
38475
+ attributes = { ...attributes, ...overrides };
38476
+ for (const [key, value] of Object.entries(attributes)) {
38477
+ if (typeof value === "function") {
38478
+ attributes[key] = await value({ seq, factory: this });
38479
+ }
38480
+ }
38481
+ return attributes;
38482
+ }
38483
+ /**
38484
+ * Create resource in database
38485
+ * @param {Object} overrides - Override attributes
38486
+ * @param {Object} options - Create options
38487
+ * @returns {Promise<Object>} Created resource
38488
+ */
38489
+ async create(overrides = {}, options = {}) {
38490
+ const { database = Factory._database } = options;
38491
+ if (!database) {
38492
+ throw new Error("Database not set. Use Factory.setDatabase(db) or pass database option");
38493
+ }
38494
+ let attributes = await this.build(overrides, options);
38495
+ for (const callback of this.beforeCreateCallbacks) {
38496
+ attributes = await callback(attributes) || attributes;
38497
+ }
38498
+ const resource = database.resources[this.resourceName];
38499
+ if (!resource) {
38500
+ throw new Error(`Resource '${this.resourceName}' not found in database`);
38501
+ }
38502
+ let created = await resource.insert(attributes);
38503
+ for (const callback of this.afterCreateCallbacks) {
38504
+ created = await callback(created, { database }) || created;
38505
+ }
38506
+ return created;
38507
+ }
38508
+ /**
38509
+ * Create multiple resources
38510
+ * @param {number} count - Number of resources to create
38511
+ * @param {Object} overrides - Override attributes
38512
+ * @param {Object} options - Create options
38513
+ * @returns {Promise<Object[]>} Created resources
38514
+ */
38515
+ async createMany(count, overrides = {}, options = {}) {
38516
+ const resources = [];
38517
+ for (let i = 0; i < count; i++) {
38518
+ const resource = await this.create(overrides, options);
38519
+ resources.push(resource);
38520
+ }
38521
+ return resources;
38522
+ }
38523
+ /**
38524
+ * Build multiple resources without creating
38525
+ * @param {number} count - Number of resources to build
38526
+ * @param {Object} overrides - Override attributes
38527
+ * @param {Object} options - Build options
38528
+ * @returns {Promise<Object[]>} Built resources
38529
+ */
38530
+ async buildMany(count, overrides = {}, options = {}) {
38531
+ const resources = [];
38532
+ for (let i = 0; i < count; i++) {
38533
+ const resource = await this.build(overrides, options);
38534
+ resources.push(resource);
38535
+ }
38536
+ return resources;
38537
+ }
38538
+ /**
38539
+ * Create with specific traits
38540
+ * @param {string|string[]} traits - Trait name(s)
38541
+ * @param {Object} overrides - Override attributes
38542
+ * @param {Object} options - Create options
38543
+ * @returns {Promise<Object>} Created resource
38544
+ */
38545
+ async createWithTraits(traits, overrides = {}, options = {}) {
38546
+ const traitArray = Array.isArray(traits) ? traits : [traits];
38547
+ return this.create(overrides, { ...options, traits: traitArray });
38548
+ }
38549
+ /**
38550
+ * Build with specific traits
38551
+ * @param {string|string[]} traits - Trait name(s)
38552
+ * @param {Object} overrides - Override attributes
38553
+ * @param {Object} options - Build options
38554
+ * @returns {Promise<Object>} Built resource
38555
+ */
38556
+ async buildWithTraits(traits, overrides = {}, options = {}) {
38557
+ const traitArray = Array.isArray(traits) ? traits : [traits];
38558
+ return this.build(overrides, { ...options, traits: traitArray });
38559
+ }
38560
+ }
38561
+
38562
+ class Seeder {
38563
+ /**
38564
+ * Constructor
38565
+ * @param {Database} database - s3db.js Database instance
38566
+ * @param {Object} options - Seeder options
38567
+ */
38568
+ constructor(database, options = {}) {
38569
+ this.database = database;
38570
+ this.options = options;
38571
+ this.verbose = options.verbose !== false;
38572
+ }
38573
+ /**
38574
+ * Log message (if verbose)
38575
+ * @param {string} message - Message to log
38576
+ * @private
38577
+ */
38578
+ log(message) {
38579
+ if (this.verbose) {
38580
+ console.log(`[Seeder] ${message}`);
38581
+ }
38582
+ }
38583
+ /**
38584
+ * Seed resources using factories
38585
+ * @param {Object} specs - Seed specifications { resourceName: count }
38586
+ * @returns {Promise<Object>} Created resources by resource name
38587
+ *
38588
+ * @example
38589
+ * const created = await seeder.seed({
38590
+ * users: 10,
38591
+ * posts: 50
38592
+ * });
38593
+ */
38594
+ async seed(specs) {
38595
+ const created = {};
38596
+ for (const [resourceName, count] of Object.entries(specs)) {
38597
+ this.log(`Seeding ${count} ${resourceName}...`);
38598
+ const factory = Factory.get(resourceName);
38599
+ if (!factory) {
38600
+ throw new Error(`Factory for '${resourceName}' not found. Define it with Factory.define()`);
38601
+ }
38602
+ created[resourceName] = await factory.createMany(count, {}, { database: this.database });
38603
+ this.log(`\u2705 Created ${count} ${resourceName}`);
38604
+ }
38605
+ return created;
38606
+ }
38607
+ /**
38608
+ * Seed with custom callback
38609
+ * @param {Function} callback - Seeding callback
38610
+ * @returns {Promise<any>} Result of callback
38611
+ *
38612
+ * @example
38613
+ * await seeder.call(async (db) => {
38614
+ * const user = await UserFactory.create();
38615
+ * const posts = await PostFactory.createMany(5, { userId: user.id });
38616
+ * return { user, posts };
38617
+ * });
38618
+ */
38619
+ async call(callback) {
38620
+ this.log("Running custom seeder...");
38621
+ const result = await callback(this.database);
38622
+ this.log("\u2705 Custom seeder completed");
38623
+ return result;
38624
+ }
38625
+ /**
38626
+ * Truncate resources (delete all data)
38627
+ * @param {string[]} resourceNames - Resource names to truncate
38628
+ * @returns {Promise<void>}
38629
+ *
38630
+ * @example
38631
+ * await seeder.truncate(['users', 'posts']);
38632
+ */
38633
+ async truncate(resourceNames) {
38634
+ for (const resourceName of resourceNames) {
38635
+ this.log(`Truncating ${resourceName}...`);
38636
+ const resource = this.database.resources[resourceName];
38637
+ if (!resource) {
38638
+ this.log(`\u26A0\uFE0F Resource '${resourceName}' not found, skipping`);
38639
+ continue;
38640
+ }
38641
+ const ids = await resource.listIds();
38642
+ if (ids.length > 0) {
38643
+ await resource.deleteMany(ids);
38644
+ this.log(`\u2705 Deleted ${ids.length} ${resourceName}`);
38645
+ } else {
38646
+ this.log(`\u2705 ${resourceName} already empty`);
38647
+ }
38648
+ }
38649
+ }
38650
+ /**
38651
+ * Truncate all resources
38652
+ * @returns {Promise<void>}
38653
+ */
38654
+ async truncateAll() {
38655
+ const resourceNames = Object.keys(this.database.resources);
38656
+ await this.truncate(resourceNames);
38657
+ }
38658
+ /**
38659
+ * Run multiple seeders in order
38660
+ * @param {Function[]} seeders - Array of seeder functions
38661
+ * @returns {Promise<Object[]>} Results of each seeder
38662
+ *
38663
+ * @example
38664
+ * await seeder.run([
38665
+ * async (db) => await UserFactory.createMany(10),
38666
+ * async (db) => await PostFactory.createMany(50)
38667
+ * ]);
38668
+ */
38669
+ async run(seeders) {
38670
+ const results = [];
38671
+ for (const seederFn of seeders) {
38672
+ this.log(`Running seeder ${seederFn.name || "anonymous"}...`);
38673
+ const result = await seederFn(this.database);
38674
+ results.push(result);
38675
+ this.log(`\u2705 Completed ${seederFn.name || "anonymous"}`);
38676
+ }
38677
+ return results;
38678
+ }
38679
+ /**
38680
+ * Seed and return specific resources
38681
+ * @param {Object} specs - Seed specifications
38682
+ * @returns {Promise<Object>} Created resources
38683
+ *
38684
+ * @example
38685
+ * const { users, posts } = await seeder.seedAndReturn({
38686
+ * users: 5,
38687
+ * posts: 10
38688
+ * });
38689
+ */
38690
+ async seedAndReturn(specs) {
38691
+ return await this.seed(specs);
38692
+ }
38693
+ /**
38694
+ * Reset database (truncate all and reset sequences)
38695
+ * @returns {Promise<void>}
38696
+ */
38697
+ async reset() {
38698
+ this.log("Resetting database...");
38699
+ await this.truncateAll();
38700
+ Factory.resetSequences();
38701
+ this.log("\u2705 Database reset complete");
38702
+ }
38703
+ }
38704
+
37647
38705
  function sanitizeLabel(value) {
37648
38706
  if (typeof value !== "string") {
37649
38707
  value = String(value);
@@ -38018,5 +39076,5 @@ var metrics = /*#__PURE__*/Object.freeze({
38018
39076
  silhouetteScore: silhouetteScore
38019
39077
  });
38020
39078
 
38021
- export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, ApiPlugin, AuditPlugin, AuthenticationError, BACKUP_DRIVERS, BackupPlugin, BaseBackupDriver, BaseError, BaseReplicator, BehaviorError, BigqueryReplicator, CONSUMER_DRIVERS, Cache, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, DynamoDBReplicator, EncryptionError, ErrorMap, EventualConsistencyPlugin, FilesystemBackupDriver, FilesystemCache, FullTextPlugin, InvalidResourceItem, MemoryCache, MetadataLimitError, MetricsPlugin, MissingMetadata, MongoDBReplicator, MultiBackupDriver, MySQLReplicator, NoSuchBucket, NoSuchKey, NotFound, PartitionAwareFilesystemCache, PartitionDriverError, PartitionError, PermissionError, PlanetScaleReplicator, Plugin, PluginError, PluginObject, PluginStorageError, PostgresReplicator, QueueConsumerPlugin, REPLICATOR_DRIVERS, RabbitMqConsumer, RelationPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3BackupDriver, S3Cache, S3QueuePlugin, Database as S3db, S3dbError, S3dbReplicator, SchedulerPlugin, Schema, SchemaError, SqsConsumer, SqsReplicator, StateMachinePlugin, StreamError, TfStatePlugin, TursoReplicator, UnknownError, ValidationError, Validator, VectorPlugin, WebhookReplicator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Cache, clearUTF8Memo, clearUTF8Memory, createBackupDriver, createConsumer, createReplicator, decode, decodeDecimal, decodeFixedPoint, decodeFixedPointBatch, decrypt, S3db as default, encode, encodeDecimal, encodeFixedPoint, encodeFixedPointBatch, encrypt, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, sha256, streamToString, transformValue, tryFn, tryFnSync, validateBackupConfig, validateReplicatorConfig };
39079
+ export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, ApiPlugin, AuditPlugin, AuthenticationError, BACKUP_DRIVERS, BackupPlugin, BaseBackupDriver, BaseError, BaseReplicator, BehaviorError, BigqueryReplicator, CONSUMER_DRIVERS, Cache, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, DynamoDBReplicator, EncryptionError, ErrorMap, EventualConsistencyPlugin, Factory, FilesystemBackupDriver, FilesystemCache, FullTextPlugin, InvalidResourceItem, MemoryCache, MetadataLimitError, MetricsPlugin, MissingMetadata, MongoDBReplicator, MultiBackupDriver, MySQLReplicator, NoSuchBucket, NoSuchKey, NotFound, PartitionAwareFilesystemCache, PartitionDriverError, PartitionError, PermissionError, PlanetScaleReplicator, Plugin, PluginError, PluginObject, PluginStorageError, PostgresReplicator, QueueConsumerPlugin, REPLICATOR_DRIVERS, RabbitMqConsumer, RelationPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3BackupDriver, S3Cache, S3QueuePlugin, Database as S3db, S3dbError, S3dbReplicator, SchedulerPlugin, Schema, SchemaError, Seeder, SqsConsumer, SqsReplicator, StateMachinePlugin, StreamError, TfStatePlugin, TursoReplicator, UnknownError, ValidationError, Validator, VectorPlugin, WebhookReplicator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Memory, createBackupDriver, createConsumer, createReplicator, decode, decodeDecimal, decodeFixedPoint, decodeFixedPointBatch, decrypt, S3db as default, encode, encodeDecimal, encodeFixedPoint, encodeFixedPointBatch, encrypt, generateTypes, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, printTypes, sha256, streamToString, transformValue, tryFn, tryFnSync, validateBackupConfig, validateReplicatorConfig };
38022
39080
  //# sourceMappingURL=s3db.es.js.map