@happyvertical/smrt-core 0.30.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.
- package/AGENTS.md +124 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +265 -0
- package/bin/smrt-prebuild.js +26 -0
- package/dist/__tests__/fixtures/advisor-test-classes.d.ts +28 -0
- package/dist/__tests__/fixtures/advisor-test-classes.d.ts.map +1 -0
- package/dist/__tests__/fixtures/collection-coverage-fixtures.d.ts +12 -0
- package/dist/__tests__/fixtures/collection-coverage-fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures/inheritance-resolver-fixtures.d.ts +28 -0
- package/dist/__tests__/fixtures/inheritance-resolver-fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures/inheritance-test-classes.d.ts +43 -0
- package/dist/__tests__/fixtures/inheritance-test-classes.d.ts.map +1 -0
- package/dist/__tests__/fixtures/mcp-integration-test-classes.d.ts +18 -0
- package/dist/__tests__/fixtures/mcp-integration-test-classes.d.ts.map +1 -0
- package/dist/__tests__/fixtures/object-ai-memory-fixtures.d.ts +15 -0
- package/dist/__tests__/fixtures/object-ai-memory-fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures/object-spec-test-classes.d.ts +13 -0
- package/dist/__tests__/fixtures/object-spec-test-classes.d.ts.map +1 -0
- package/dist/__tests__/fixtures/registry-test-classes.d.ts +23 -0
- package/dist/__tests__/fixtures/registry-test-classes.d.ts.map +1 -0
- package/dist/adapters/ai-usage.d.ts +23 -0
- package/dist/adapters/ai-usage.d.ts.map +1 -0
- package/dist/adapters/ai-usage.js +105 -0
- package/dist/adapters/ai-usage.js.map +1 -0
- package/dist/adapters/cost-rates.d.ts +20 -0
- package/dist/adapters/cost-rates.d.ts.map +1 -0
- package/dist/adapters/cost-rates.js +40 -0
- package/dist/adapters/cost-rates.js.map +1 -0
- package/dist/adapters/index.d.ts +19 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/metrics.d.ts +111 -0
- package/dist/adapters/metrics.d.ts.map +1 -0
- package/dist/adapters/metrics.js +169 -0
- package/dist/adapters/metrics.js.map +1 -0
- package/dist/adapters/pubsub.d.ts +124 -0
- package/dist/adapters/pubsub.d.ts.map +1 -0
- package/dist/adapters/pubsub.js +121 -0
- package/dist/adapters/pubsub.js.map +1 -0
- package/dist/browser.d.ts +32 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +68 -0
- package/dist/browser.js.map +1 -0
- package/dist/child-accessors.d.ts +27 -0
- package/dist/child-accessors.d.ts.map +1 -0
- package/dist/child-accessors.js +35 -0
- package/dist/child-accessors.js.map +1 -0
- package/dist/class.d.ts +313 -0
- package/dist/class.d.ts.map +1 -0
- package/dist/class.js +896 -0
- package/dist/class.js.map +1 -0
- package/dist/collection-cache.d.ts +110 -0
- package/dist/collection-cache.d.ts.map +1 -0
- package/dist/collection-cache.js +187 -0
- package/dist/collection-cache.js.map +1 -0
- package/dist/collection.d.ts +894 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +1987 -0
- package/dist/collection.js.map +1 -0
- package/dist/config/global-config.d.ts +3 -0
- package/dist/config/global-config.d.ts.map +1 -0
- package/dist/config/global-config.js +19 -0
- package/dist/config/global-config.js.map +1 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -0
- package/dist/consumer-plugin/index.d.ts +22 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -0
- package/dist/consumer-plugin/index.js +452 -0
- package/dist/consumer-plugin/index.js.map +1 -0
- package/dist/consumer-plugin.d.ts +8 -0
- package/dist/consumer-plugin.d.ts.map +1 -0
- package/dist/consumer-plugin.js +5 -0
- package/dist/consumer-plugin.js.map +1 -0
- package/dist/database.d.ts +95 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +32 -0
- package/dist/database.js.map +1 -0
- package/dist/decorators/compatibility.d.ts +14 -0
- package/dist/decorators/compatibility.d.ts.map +1 -0
- package/dist/decorators/compatibility.js +111 -0
- package/dist/decorators/compatibility.js.map +1 -0
- package/dist/decorators/index.d.ts +381 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +104 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/dispatch/bus.d.ts +306 -0
- package/dist/dispatch/bus.d.ts.map +1 -0
- package/dist/dispatch/bus.js +583 -0
- package/dist/dispatch/bus.js.map +1 -0
- package/dist/dispatch/collections/DispatchSubscriptions.d.ts +79 -0
- package/dist/dispatch/collections/DispatchSubscriptions.d.ts.map +1 -0
- package/dist/dispatch/collections/DispatchSubscriptions.js +243 -0
- package/dist/dispatch/collections/DispatchSubscriptions.js.map +1 -0
- package/dist/dispatch/collections/Dispatches.d.ts +98 -0
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -0
- package/dist/dispatch/collections/Dispatches.js +358 -0
- package/dist/dispatch/collections/Dispatches.js.map +1 -0
- package/dist/dispatch/index.d.ts +47 -0
- package/dist/dispatch/index.d.ts.map +1 -0
- package/dist/dispatch/models/Dispatch.d.ts +101 -0
- package/dist/dispatch/models/Dispatch.d.ts.map +1 -0
- package/dist/dispatch/models/Dispatch.js +162 -0
- package/dist/dispatch/models/Dispatch.js.map +1 -0
- package/dist/dispatch/models/DispatchSubscription.d.ts +83 -0
- package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -0
- package/dist/dispatch/models/DispatchSubscription.js +112 -0
- package/dist/dispatch/models/DispatchSubscription.js.map +1 -0
- package/dist/dispatch/tenant-resolver.d.ts +98 -0
- package/dist/dispatch/tenant-resolver.d.ts.map +1 -0
- package/dist/dispatch/tenant-resolver.js +32 -0
- package/dist/dispatch/tenant-resolver.js.map +1 -0
- package/dist/dispatch/types.d.ts +149 -0
- package/dist/dispatch/types.d.ts.map +1 -0
- package/dist/embeddings/hash.d.ts +33 -0
- package/dist/embeddings/hash.d.ts.map +1 -0
- package/dist/embeddings/hash.js +37 -0
- package/dist/embeddings/hash.js.map +1 -0
- package/dist/embeddings/index.d.ts +36 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/provider.d.ts +75 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +170 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/embeddings/similarity.d.ts +47 -0
- package/dist/embeddings/similarity.d.ts.map +1 -0
- package/dist/embeddings/similarity.js +64 -0
- package/dist/embeddings/similarity.js.map +1 -0
- package/dist/embeddings/storage.d.ts +125 -0
- package/dist/embeddings/storage.d.ts.map +1 -0
- package/dist/embeddings/storage.js +283 -0
- package/dist/embeddings/storage.js.map +1 -0
- package/dist/embeddings/types.d.ts +250 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/errors.d.ts +363 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +669 -0
- package/dist/errors.js.map +1 -0
- package/dist/generators/cli.d.ts +162 -0
- package/dist/generators/cli.d.ts.map +1 -0
- package/dist/generators/cli.js +462 -0
- package/dist/generators/cli.js.map +1 -0
- package/dist/generators/index.d.ts +13 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/mcp-runtime-template.d.ts +60 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -0
- package/dist/generators/mcp-runtime-template.js +509 -0
- package/dist/generators/mcp-runtime-template.js.map +1 -0
- package/dist/generators/mcp.d.ts +231 -0
- package/dist/generators/mcp.d.ts.map +1 -0
- package/dist/generators/mcp.js +1220 -0
- package/dist/generators/mcp.js.map +1 -0
- package/dist/generators/rest.d.ts +171 -0
- package/dist/generators/rest.d.ts.map +1 -0
- package/dist/generators/rest.js +591 -0
- package/dist/generators/rest.js.map +1 -0
- package/dist/generators/swagger.d.ts +21 -0
- package/dist/generators/swagger.d.ts.map +1 -0
- package/dist/generators/swagger.js +307 -0
- package/dist/generators/swagger.js.map +1 -0
- package/dist/generators/tenant-gate.d.ts +74 -0
- package/dist/generators/tenant-gate.d.ts.map +1 -0
- package/dist/generators/tenant-gate.js +15 -0
- package/dist/generators/tenant-gate.js.map +1 -0
- package/dist/generators.d.ts +8 -0
- package/dist/generators.d.ts.map +1 -0
- package/dist/generators.js +19 -0
- package/dist/generators.js.map +1 -0
- package/dist/hierarchical.d.ts +103 -0
- package/dist/hierarchical.d.ts.map +1 -0
- package/dist/hierarchical.js +184 -0
- package/dist/hierarchical.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors.d.ts +251 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +259 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/junction.d.ts +99 -0
- package/dist/junction.d.ts.map +1 -0
- package/dist/junction.js +136 -0
- package/dist/junction.js.map +1 -0
- package/dist/knowledge.d.ts +11 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +310 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/lazy-config.d.ts +160 -0
- package/dist/lazy-config.d.ts.map +1 -0
- package/dist/lazy-config.js +146 -0
- package/dist/lazy-config.js.map +1 -0
- package/dist/manifest/discover-base-classes.d.ts +78 -0
- package/dist/manifest/discover-base-classes.d.ts.map +1 -0
- package/dist/manifest/discover-base-classes.js +85 -0
- package/dist/manifest/discover-base-classes.js.map +1 -0
- package/dist/manifest/discover-smrt-packages.d.ts +48 -0
- package/dist/manifest/discover-smrt-packages.d.ts.map +1 -0
- package/dist/manifest/discover-smrt-packages.js +361 -0
- package/dist/manifest/discover-smrt-packages.js.map +1 -0
- package/dist/manifest/generator.d.ts +93 -0
- package/dist/manifest/generator.d.ts.map +1 -0
- package/dist/manifest/generator.js +380 -0
- package/dist/manifest/generator.js.map +1 -0
- package/dist/manifest/index.d.ts +16 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +51 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manager.d.ts +51 -0
- package/dist/manifest/manager.d.ts.map +1 -0
- package/dist/manifest/manager.js +89 -0
- package/dist/manifest/manager.js.map +1 -0
- package/dist/manifest/manifest-loader.d.ts +187 -0
- package/dist/manifest/manifest-loader.d.ts.map +1 -0
- package/dist/manifest/manifest-loader.js +847 -0
- package/dist/manifest/manifest-loader.js.map +1 -0
- package/dist/manifest/sources/composite.d.ts +22 -0
- package/dist/manifest/sources/composite.d.ts.map +1 -0
- package/dist/manifest/sources/composite.js +60 -0
- package/dist/manifest/sources/composite.js.map +1 -0
- package/dist/manifest/sources/embedded.d.ts +7 -0
- package/dist/manifest/sources/embedded.d.ts.map +1 -0
- package/dist/manifest/sources/embedded.js +30 -0
- package/dist/manifest/sources/embedded.js.map +1 -0
- package/dist/manifest/sources/explicit-paths.d.ts +17 -0
- package/dist/manifest/sources/explicit-paths.d.ts.map +1 -0
- package/dist/manifest/sources/explicit-paths.js +35 -0
- package/dist/manifest/sources/explicit-paths.js.map +1 -0
- package/dist/manifest/sources/fallback.d.ts +25 -0
- package/dist/manifest/sources/fallback.d.ts.map +1 -0
- package/dist/manifest/sources/fallback.js +63 -0
- package/dist/manifest/sources/fallback.js.map +1 -0
- package/dist/manifest/sources/index.d.ts +17 -0
- package/dist/manifest/sources/index.d.ts.map +1 -0
- package/dist/manifest/sources/local-test.d.ts +7 -0
- package/dist/manifest/sources/local-test.d.ts.map +1 -0
- package/dist/manifest/sources/local-test.js +21 -0
- package/dist/manifest/sources/local-test.js.map +1 -0
- package/dist/manifest/sources/static.d.ts +7 -0
- package/dist/manifest/sources/static.d.ts.map +1 -0
- package/dist/manifest/sources/static.js +19 -0
- package/dist/manifest/sources/static.js.map +1 -0
- package/dist/manifest/sources/test.d.ts +7 -0
- package/dist/manifest/sources/test.d.ts.map +1 -0
- package/dist/manifest/sources/test.js +21 -0
- package/dist/manifest/sources/test.js.map +1 -0
- package/dist/manifest/sources/types.d.ts +79 -0
- package/dist/manifest/sources/types.d.ts.map +1 -0
- package/dist/manifest/sources/types.js +61 -0
- package/dist/manifest/sources/types.js.map +1 -0
- package/dist/manifest/static-manifest.d.ts +4 -0
- package/dist/manifest/static-manifest.d.ts.map +1 -0
- package/dist/manifest/static-manifest.js +1535 -0
- package/dist/manifest/static-manifest.js.map +1 -0
- package/dist/manifest/store.d.ts +111 -0
- package/dist/manifest/store.d.ts.map +1 -0
- package/dist/manifest/store.js +431 -0
- package/dist/manifest/store.js.map +1 -0
- package/dist/manifest/test-manifest-loader.d.ts +3 -0
- package/dist/manifest/test-manifest-loader.d.ts.map +1 -0
- package/dist/manifest/test-manifest-stub.d.ts +4 -0
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -0
- package/dist/manifest/test-manifest-stub.js +80013 -0
- package/dist/manifest/test-manifest-stub.js.map +1 -0
- package/dist/manifest.d.ts +8 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +20 -0
- package/dist/manifest.js.map +1 -0
- package/dist/manifest.json +1489 -0
- package/dist/mcp-advisor/index.d.ts +499 -0
- package/dist/mcp-advisor/index.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/add-ai-methods.d.ts +6 -0
- package/dist/mcp-advisor/tools/add-ai-methods.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/configure-decorators.d.ts +6 -0
- package/dist/mcp-advisor/tools/configure-decorators.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/generate-collection.d.ts +6 -0
- package/dist/mcp-advisor/tools/generate-collection.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/generate-field-definitions.d.ts +6 -0
- package/dist/mcp-advisor/tools/generate-field-definitions.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/generate-smrt-class.d.ts +6 -0
- package/dist/mcp-advisor/tools/generate-smrt-class.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/get-object-config.d.ts +6 -0
- package/dist/mcp-advisor/tools/get-object-config.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/get-object-schema.d.ts +10 -0
- package/dist/mcp-advisor/tools/get-object-schema.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/list-registered-objects.d.ts +9 -0
- package/dist/mcp-advisor/tools/list-registered-objects.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +9 -0
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/preview-mcp-tools.d.ts +9 -0
- package/dist/mcp-advisor/tools/preview-mcp-tools.d.ts.map +1 -0
- package/dist/mcp-advisor/tools/validate-smrt-object.d.ts +6 -0
- package/dist/mcp-advisor/tools/validate-smrt-object.d.ts.map +1 -0
- package/dist/mcp-advisor/types.d.ts +209 -0
- package/dist/mcp-advisor/types.d.ts.map +1 -0
- package/dist/migrations/backfill-tracker.d.ts +84 -0
- package/dist/migrations/backfill-tracker.d.ts.map +1 -0
- package/dist/migrations/backfill-tracker.js +118 -0
- package/dist/migrations/backfill-tracker.js.map +1 -0
- package/dist/migrations/checksum.d.ts +43 -0
- package/dist/migrations/checksum.d.ts.map +1 -0
- package/dist/migrations/checksum.js +32 -0
- package/dist/migrations/checksum.js.map +1 -0
- package/dist/migrations/differ.d.ts +186 -0
- package/dist/migrations/differ.d.ts.map +1 -0
- package/dist/migrations/differ.js +601 -0
- package/dist/migrations/differ.js.map +1 -0
- package/dist/migrations/generator.d.ts +133 -0
- package/dist/migrations/generator.d.ts.map +1 -0
- package/dist/migrations/generator.js +328 -0
- package/dist/migrations/generator.js.map +1 -0
- package/dist/migrations/index.d.ts +20 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/orchestrate.d.ts +148 -0
- package/dist/migrations/orchestrate.d.ts.map +1 -0
- package/dist/migrations/orchestrate.js +118 -0
- package/dist/migrations/orchestrate.js.map +1 -0
- package/dist/migrations/tracker.d.ts +134 -0
- package/dist/migrations/tracker.d.ts.map +1 -0
- package/dist/migrations/tracker.js +624 -0
- package/dist/migrations/tracker.js.map +1 -0
- package/dist/migrations/types.d.ts +221 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations.d.ts +7 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +26 -0
- package/dist/migrations.js.map +1 -0
- package/dist/node_modules/.pnpm/balanced-match@4.0.4/node_modules/balanced-match/dist/esm/index.js +56 -0
- package/dist/node_modules/.pnpm/balanced-match@4.0.4/node_modules/balanced-match/dist/esm/index.js.map +1 -0
- package/dist/node_modules/.pnpm/brace-expansion@5.0.5/node_modules/brace-expansion/dist/esm/index.js +163 -0
- package/dist/node_modules/.pnpm/brace-expansion@5.0.5/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/assert-valid-pattern.js +13 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/ast.js +654 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/ast.js.map +1 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/brace-expressions.js +111 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/escape.js +10 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/escape.js.map +1 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/index.js +824 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/index.js.map +1 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/unescape.js +10 -0
- package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/unescape.js.map +1 -0
- package/dist/object.d.ts +1202 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +2731 -0
- package/dist/object.js.map +1 -0
- package/dist/polymorphic-association.d.ts +69 -0
- package/dist/polymorphic-association.d.ts.map +1 -0
- package/dist/polymorphic-association.js +96 -0
- package/dist/polymorphic-association.js.map +1 -0
- package/dist/prebuild/cli.d.ts +7 -0
- package/dist/prebuild/cli.d.ts.map +1 -0
- package/dist/prebuild/cli.js +29 -0
- package/dist/prebuild/cli.js.map +1 -0
- package/dist/prebuild/index.d.ts +22 -0
- package/dist/prebuild/index.d.ts.map +1 -0
- package/dist/prebuild/index.js +239 -0
- package/dist/prebuild/index.js.map +1 -0
- package/dist/prebuild.d.ts +8 -0
- package/dist/prebuild.d.ts.map +1 -0
- package/dist/prebuild.js +6 -0
- package/dist/prebuild.js.map +1 -0
- package/dist/registry/cache-config.d.ts +13 -0
- package/dist/registry/cache-config.d.ts.map +1 -0
- package/dist/registry/cache-config.js +17 -0
- package/dist/registry/cache-config.js.map +1 -0
- package/dist/registry/class-registration.d.ts +31 -0
- package/dist/registry/class-registration.d.ts.map +1 -0
- package/dist/registry/class-registration.js +1074 -0
- package/dist/registry/class-registration.js.map +1 -0
- package/dist/registry/collection-resolution.d.ts +45 -0
- package/dist/registry/collection-resolution.d.ts.map +1 -0
- package/dist/registry/collection-resolution.js +121 -0
- package/dist/registry/collection-resolution.js.map +1 -0
- package/dist/registry/collision-policy.d.ts +179 -0
- package/dist/registry/collision-policy.d.ts.map +1 -0
- package/dist/registry/collision-policy.js +153 -0
- package/dist/registry/collision-policy.js.map +1 -0
- package/dist/registry/diagnostics.d.ts +58 -0
- package/dist/registry/diagnostics.d.ts.map +1 -0
- package/dist/registry/diagnostics.js +54 -0
- package/dist/registry/diagnostics.js.map +1 -0
- package/dist/registry/embedding-manager.d.ts +23 -0
- package/dist/registry/embedding-manager.d.ts.map +1 -0
- package/dist/registry/embedding-manager.js +62 -0
- package/dist/registry/embedding-manager.js.map +1 -0
- package/dist/registry/index.d.ts +13 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/inheritance-resolver.d.ts +13 -0
- package/dist/registry/inheritance-resolver.d.ts.map +1 -0
- package/dist/registry/inheritance-resolver.js +244 -0
- package/dist/registry/inheritance-resolver.js.map +1 -0
- package/dist/registry/manifest-field-merge.d.ts +4 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -0
- package/dist/registry/manifest-field-merge.js +82 -0
- package/dist/registry/manifest-field-merge.js.map +1 -0
- package/dist/registry/name-resolver.d.ts +102 -0
- package/dist/registry/name-resolver.d.ts.map +1 -0
- package/dist/registry/name-resolver.js +241 -0
- package/dist/registry/name-resolver.js.map +1 -0
- package/dist/registry/relationship-graph.d.ts +16 -0
- package/dist/registry/relationship-graph.d.ts.map +1 -0
- package/dist/registry/relationship-graph.js +79 -0
- package/dist/registry/relationship-graph.js.map +1 -0
- package/dist/registry/schema-builder.d.ts +113 -0
- package/dist/registry/schema-builder.d.ts.map +1 -0
- package/dist/registry/schema-builder.js +474 -0
- package/dist/registry/schema-builder.js.map +1 -0
- package/dist/registry/shared-state.d.ts +62 -0
- package/dist/registry/shared-state.d.ts.map +1 -0
- package/dist/registry/shared-state.js +135 -0
- package/dist/registry/shared-state.js.map +1 -0
- package/dist/registry/types.d.ts +667 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/validator.d.ts +13 -0
- package/dist/registry/validator.d.ts.map +1 -0
- package/dist/registry/validator.js +138 -0
- package/dist/registry/validator.js.map +1 -0
- package/dist/registry.d.ts +1358 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +2301 -0
- package/dist/registry.js.map +1 -0
- package/dist/runtime/client.d.ts +34 -0
- package/dist/runtime/client.d.ts.map +1 -0
- package/dist/runtime/client.js +104 -0
- package/dist/runtime/client.js.map +1 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/mcp.d.ts +47 -0
- package/dist/runtime/mcp.d.ts.map +1 -0
- package/dist/runtime/mcp.js +72 -0
- package/dist/runtime/mcp.js.map +1 -0
- package/dist/runtime/server.d.ts +92 -0
- package/dist/runtime/server.d.ts.map +1 -0
- package/dist/runtime/server.js +390 -0
- package/dist/runtime/server.js.map +1 -0
- package/dist/runtime/types.d.ts +58 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +10 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scanner/import-scanner.d.ts +7 -0
- package/dist/scanner/import-scanner.d.ts.map +1 -0
- package/dist/scanner/index.d.ts +12 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/manifest-generator.d.ts +304 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -0
- package/dist/scanner/manifest-generator.js +1707 -0
- package/dist/scanner/manifest-generator.js.map +1 -0
- package/dist/scanner/test-file-patterns.d.ts +18 -0
- package/dist/scanner/test-file-patterns.d.ts.map +1 -0
- package/dist/scanner/test-file-patterns.js +16 -0
- package/dist/scanner/test-file-patterns.js.map +1 -0
- package/dist/scanner/types.d.ts +313 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +2 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/scanner.d.ts +6 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +6 -0
- package/dist/scanner.js.map +1 -0
- package/dist/schema/code-generator.d.ts +53 -0
- package/dist/schema/code-generator.d.ts.map +1 -0
- package/dist/schema/ddl/base-strategy.d.ts +80 -0
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -0
- package/dist/schema/ddl/base-strategy.js +240 -0
- package/dist/schema/ddl/base-strategy.js.map +1 -0
- package/dist/schema/ddl/duckdb-strategy.d.ts +33 -0
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -0
- package/dist/schema/ddl/duckdb-strategy.js +74 -0
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -0
- package/dist/schema/ddl/index.d.ts +53 -0
- package/dist/schema/ddl/index.d.ts.map +1 -0
- package/dist/schema/ddl/index.js +80 -0
- package/dist/schema/ddl/index.js.map +1 -0
- package/dist/schema/ddl/json-duckdb-strategy.d.ts +8 -0
- package/dist/schema/ddl/json-duckdb-strategy.d.ts.map +1 -0
- package/dist/schema/ddl/json-duckdb-strategy.js +14 -0
- package/dist/schema/ddl/json-duckdb-strategy.js.map +1 -0
- package/dist/schema/ddl/postgres-strategy.d.ts +29 -0
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -0
- package/dist/schema/ddl/postgres-strategy.js +102 -0
- package/dist/schema/ddl/postgres-strategy.js.map +1 -0
- package/dist/schema/ddl/sqlite-strategy.d.ts +38 -0
- package/dist/schema/ddl/sqlite-strategy.d.ts.map +1 -0
- package/dist/schema/ddl/sqlite-strategy.js +74 -0
- package/dist/schema/ddl/sqlite-strategy.js.map +1 -0
- package/dist/schema/ddl/types.d.ts +114 -0
- package/dist/schema/ddl/types.d.ts.map +1 -0
- package/dist/schema/generator.d.ts +176 -0
- package/dist/schema/generator.d.ts.map +1 -0
- package/dist/schema/generator.js +1076 -0
- package/dist/schema/generator.js.map +1 -0
- package/dist/schema/index-utils.d.ts +19 -0
- package/dist/schema/index-utils.d.ts.map +1 -0
- package/dist/schema/index-utils.js +32 -0
- package/dist/schema/index-utils.js.map +1 -0
- package/dist/schema/index.d.ts +13 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/override-system.d.ts +43 -0
- package/dist/schema/override-system.d.ts.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +112 -0
- package/dist/schema/schema-aggregator.d.ts.map +1 -0
- package/dist/schema/schema-manager.d.ts +95 -0
- package/dist/schema/schema-manager.d.ts.map +1 -0
- package/dist/schema/schema-manager.js +283 -0
- package/dist/schema/schema-manager.js.map +1 -0
- package/dist/schema/sql-identifiers.d.ts +107 -0
- package/dist/schema/sql-identifiers.d.ts.map +1 -0
- package/dist/schema/sql-identifiers.js +190 -0
- package/dist/schema/sql-identifiers.js.map +1 -0
- package/dist/schema/table-verifier.d.ts +10 -0
- package/dist/schema/table-verifier.d.ts.map +1 -0
- package/dist/schema/table-verifier.js +37 -0
- package/dist/schema/table-verifier.js.map +1 -0
- package/dist/schema/types.d.ts +241 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/utils.d.ts +32 -0
- package/dist/schema/utils.d.ts.map +1 -0
- package/dist/schema/utils.js +134 -0
- package/dist/schema/utils.js.map +1 -0
- package/dist/scripts/create-wrappers.js +89 -0
- package/dist/scripts/generate-manifest.js +155 -0
- package/dist/scripts/generate-test-manifest.js +77 -0
- package/dist/scripts/migrate-datetime-to-timestamp.ts +310 -0
- package/dist/scripts/prepack.js +49 -0
- package/dist/signals/bus.d.ts +64 -0
- package/dist/signals/bus.d.ts.map +1 -0
- package/dist/signals/bus.js +102 -0
- package/dist/signals/bus.js.map +1 -0
- package/dist/signals/index.d.ts +11 -0
- package/dist/signals/index.d.ts.map +1 -0
- package/dist/signals/sanitizer.d.ts +54 -0
- package/dist/signals/sanitizer.d.ts.map +1 -0
- package/dist/signals/sanitizer.js +111 -0
- package/dist/signals/sanitizer.js.map +1 -0
- package/dist/smrt-knowledge.json +335 -0
- package/dist/system/compatibility.d.ts +8 -0
- package/dist/system/compatibility.d.ts.map +1 -0
- package/dist/system/compatibility.js +409 -0
- package/dist/system/compatibility.js.map +1 -0
- package/dist/system/index.d.ts +9 -0
- package/dist/system/index.d.ts.map +1 -0
- package/dist/system/schema.d.ts +69 -0
- package/dist/system/schema.d.ts.map +1 -0
- package/dist/system/schema.js +271 -0
- package/dist/system/schema.js.map +1 -0
- package/dist/system/types.d.ts +135 -0
- package/dist/system/types.d.ts.map +1 -0
- package/dist/system-fields.d.ts +44 -0
- package/dist/system-fields.d.ts.map +1 -0
- package/dist/system-fields.js +55 -0
- package/dist/system-fields.js.map +1 -0
- package/dist/table-cache.d.ts +28 -0
- package/dist/table-cache.d.ts.map +1 -0
- package/dist/table-cache.js +21 -0
- package/dist/table-cache.js.map +1 -0
- package/dist/test-utils.d.ts +140 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/testing/database.d.ts +73 -0
- package/dist/testing/database.d.ts.map +1 -0
- package/dist/testing/database.js +204 -0
- package/dist/testing/database.js.map +1 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/tool-executor.d.ts +101 -0
- package/dist/tools/tool-executor.d.ts.map +1 -0
- package/dist/tools/tool-executor.js +142 -0
- package/dist/tools/tool-executor.js.map +1 -0
- package/dist/tools/tool-generator.d.ts +54 -0
- package/dist/tools/tool-generator.d.ts.map +1 -0
- package/dist/tools/tool-generator.js +121 -0
- package/dist/tools/tool-generator.js.map +1 -0
- package/dist/utils/chunk.d.ts +32 -0
- package/dist/utils/chunk.d.ts.map +1 -0
- package/dist/utils/chunk.js +14 -0
- package/dist/utils/chunk.js.map +1 -0
- package/dist/utils/import-workspace-module.d.ts +8 -0
- package/dist/utils/import-workspace-module.d.ts.map +1 -0
- package/dist/utils/import-workspace-module.js +81 -0
- package/dist/utils/import-workspace-module.js.map +1 -0
- package/dist/utils/json.d.ts +102 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +43 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/lru-cache.d.ts +69 -0
- package/dist/utils/lru-cache.d.ts.map +1 -0
- package/dist/utils/lru-cache.js +100 -0
- package/dist/utils/lru-cache.js.map +1 -0
- package/dist/utils/naming.d.ts +16 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +23 -0
- package/dist/utils/naming.js.map +1 -0
- package/dist/utils/qualified-names.d.ts +122 -0
- package/dist/utils/qualified-names.d.ts.map +1 -0
- package/dist/utils/qualified-names.js +82 -0
- package/dist/utils/qualified-names.js.map +1 -0
- package/dist/utils/scanner-module.d.ts +37 -0
- package/dist/utils/scanner-module.d.ts.map +1 -0
- package/dist/utils.d.ts +177 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +185 -0
- package/dist/utils.js.map +1 -0
- package/dist/vite-plugin/import-build-aware.d.ts +68 -0
- package/dist/vite-plugin/import-build-aware.d.ts.map +1 -0
- package/dist/vite-plugin/import-build-aware.js +72 -0
- package/dist/vite-plugin/import-build-aware.js.map +1 -0
- package/dist/vite-plugin/index.d.ts +59 -0
- package/dist/vite-plugin/index.d.ts.map +1 -0
- package/dist/vite-plugin/index.js +1400 -0
- package/dist/vite-plugin/index.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +66 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.js +1375 -0
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -0
- package/dist/vite-plugin/templates/default-ui.ts +432 -0
- package/dist/vite-plugin/templates/default.html +206 -0
- package/dist/vite-plugin.d.ts +8 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +11 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +208 -0
|
@@ -0,0 +1,1987 @@
|
|
|
1
|
+
import { createLogger } from "@happyvertical/logger";
|
|
2
|
+
import { buildWhere } from "@happyvertical/sql";
|
|
3
|
+
import { SmrtClass } from "./class.js";
|
|
4
|
+
import { resolveDbCacheKey, buildQueryCacheKey, ensureCacheInvalidationListener, registerCrossProcessCacheInterest, getCachedRows, getCacheGeneration, setCachedRows, invalidateCollectionCache } from "./collection-cache.js";
|
|
5
|
+
import { EmbeddingProvider } from "./embeddings/provider.js";
|
|
6
|
+
import { EmbeddingStorage } from "./embeddings/storage.js";
|
|
7
|
+
import { createInterceptorContext, GlobalInterceptors } from "./interceptors.js";
|
|
8
|
+
import { ObjectRegistry } from "./registry.js";
|
|
9
|
+
import { verifyPersistenceTable } from "./schema/table-verifier.js";
|
|
10
|
+
import { fieldsFromClass, toCamelCase, formatDataJs } from "./utils.js";
|
|
11
|
+
import { chunkArray, IN_LIST_CHUNK_SIZE } from "./utils/chunk.js";
|
|
12
|
+
import { toSnakeCase, classnameToTablename } from "./utils/naming.js";
|
|
13
|
+
const logger = createLogger({ level: "info" });
|
|
14
|
+
function resolveMetaTypeInWhere(where) {
|
|
15
|
+
if (!where?._meta_type || typeof where._meta_type !== "string") {
|
|
16
|
+
return where;
|
|
17
|
+
}
|
|
18
|
+
const metaTypeValue = where._meta_type;
|
|
19
|
+
if (metaTypeValue.includes(":")) {
|
|
20
|
+
return where;
|
|
21
|
+
}
|
|
22
|
+
const registeredClass = ObjectRegistry.getClass(metaTypeValue);
|
|
23
|
+
if (registeredClass?.qualifiedName) {
|
|
24
|
+
return {
|
|
25
|
+
...where,
|
|
26
|
+
_meta_type: registeredClass.qualifiedName
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return where;
|
|
30
|
+
}
|
|
31
|
+
class SmrtCollection extends SmrtClass {
|
|
32
|
+
/**
|
|
33
|
+
* Cached fields for sync access during queries.
|
|
34
|
+
* Populated during create() to avoid async getFields() calls on every query.
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
_cachedFields = null;
|
|
38
|
+
getRegisteredItemClass() {
|
|
39
|
+
return ObjectRegistry.getClassByConstructor(this._itemClass) || ObjectRegistry.getClass(this._itemClass.name);
|
|
40
|
+
}
|
|
41
|
+
getResolvedItemClassName() {
|
|
42
|
+
return this.getRegisteredItemClass()?.name || this._itemClass.name;
|
|
43
|
+
}
|
|
44
|
+
getResolvedItemQualifiedName() {
|
|
45
|
+
const registered = this.getRegisteredItemClass();
|
|
46
|
+
return registered?.qualifiedName || registered?.name || this._itemClass.name;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert WHERE clause field names from camelCase to snake_case while preserving operators.
|
|
50
|
+
* Validates operators and field names to prevent SQL injection and invalid queries.
|
|
51
|
+
*
|
|
52
|
+
* Uses cached fields for sync access (issue #663) to avoid async overhead on every query.
|
|
53
|
+
*
|
|
54
|
+
* @param where - WHERE clause object with camelCase field names
|
|
55
|
+
* @returns WHERE clause object with snake_case field names
|
|
56
|
+
* @private
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* // Input: { 'typeId': 'foo', 'categoryId >': 100 }
|
|
61
|
+
* // Output: { 'type_id': 'foo', 'category_id >': 100 }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
convertWhereKeys(where) {
|
|
65
|
+
const VALID_OPERATORS = [
|
|
66
|
+
"=",
|
|
67
|
+
">",
|
|
68
|
+
"<",
|
|
69
|
+
">=",
|
|
70
|
+
"<=",
|
|
71
|
+
"!=",
|
|
72
|
+
"in",
|
|
73
|
+
"not in",
|
|
74
|
+
"like",
|
|
75
|
+
"contains"
|
|
76
|
+
];
|
|
77
|
+
const fields = this.getFieldsSync();
|
|
78
|
+
const validFieldNames = new Set(
|
|
79
|
+
Object.keys(fields).map((f) => toSnakeCase(f))
|
|
80
|
+
);
|
|
81
|
+
const sensitiveFieldNames = /* @__PURE__ */ new Set();
|
|
82
|
+
const collectSensitive = (fieldMap) => {
|
|
83
|
+
const entries = fieldMap instanceof Map ? fieldMap.entries() : Object.entries(fieldMap);
|
|
84
|
+
for (const [fieldName, def] of entries) {
|
|
85
|
+
if (def && (def.sensitive === true || def._meta?.sensitive === true)) {
|
|
86
|
+
sensitiveFieldNames.add(toSnakeCase(fieldName));
|
|
87
|
+
sensitiveFieldNames.add(fieldName);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
collectSensitive(fields);
|
|
92
|
+
validFieldNames.add("id");
|
|
93
|
+
validFieldNames.add("slug");
|
|
94
|
+
validFieldNames.add("context");
|
|
95
|
+
validFieldNames.add("created_at");
|
|
96
|
+
validFieldNames.add("updated_at");
|
|
97
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
98
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
99
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(itemQualifiedName);
|
|
100
|
+
if (tableStrategy === "sti") {
|
|
101
|
+
validFieldNames.add("_meta_type");
|
|
102
|
+
validFieldNames.add("meta_type");
|
|
103
|
+
validFieldNames.add("_meta_data");
|
|
104
|
+
validFieldNames.add("meta_data");
|
|
105
|
+
const inheritanceChain = ObjectRegistry.getInheritanceChain(itemQualifiedName);
|
|
106
|
+
for (const ancestorName of inheritanceChain) {
|
|
107
|
+
if (ancestorName === "SmrtObject" || ancestorName === "SmrtClass" || ancestorName === itemQualifiedName || ancestorName === itemClassName) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const ancestorFields = ObjectRegistry.getFields(ancestorName);
|
|
111
|
+
for (const fieldName of ancestorFields.keys()) {
|
|
112
|
+
validFieldNames.add(toSnakeCase(fieldName));
|
|
113
|
+
}
|
|
114
|
+
collectSensitive(ancestorFields);
|
|
115
|
+
}
|
|
116
|
+
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
117
|
+
if (stiBase) {
|
|
118
|
+
for (const descendant of ObjectRegistry.getDescendants(stiBase)) {
|
|
119
|
+
collectSensitive(ObjectRegistry.getFields(descendant));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const hasRegisteredFields = Object.keys(fields).length > 0;
|
|
124
|
+
const skipFieldValidation = !hasRegisteredFields;
|
|
125
|
+
const converted = {};
|
|
126
|
+
if (Object.hasOwn(where, "__proto__") || Object.hasOwn(where, "constructor") || Object.hasOwn(where, "prototype")) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid WHERE clause: Prototype pollution attempts are not allowed. Detected dangerous properties in WHERE clause.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
for (const [key, value] of Object.entries(where)) {
|
|
132
|
+
const parts = key.trim().split(/\s+/);
|
|
133
|
+
const fieldName = parts[0];
|
|
134
|
+
const operator = parts.slice(1).join(" ") || "=";
|
|
135
|
+
if (fieldName === "__proto__" || fieldName === "constructor" || fieldName === "prototype" || fieldName.includes("__proto__") || fieldName.includes("constructor") || fieldName.includes("prototype")) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Invalid WHERE clause field: '${fieldName}'. Prototype pollution attempts are not allowed.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const dotIndex = fieldName.indexOf(".");
|
|
141
|
+
const baseFieldName = dotIndex >= 0 ? fieldName.substring(0, dotIndex) : fieldName;
|
|
142
|
+
const jsonPath = dotIndex >= 0 ? fieldName.substring(dotIndex) : null;
|
|
143
|
+
const snakeBaseFieldName = baseFieldName.startsWith("_") ? `_${toSnakeCase(baseFieldName.slice(1))}` : toSnakeCase(baseFieldName);
|
|
144
|
+
const snakeFieldName = jsonPath ? `${snakeBaseFieldName}${jsonPath}` : snakeBaseFieldName;
|
|
145
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)*$/.test(snakeFieldName)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Invalid WHERE clause field: '${fieldName}'. Field names must be identifiers (letters, digits, underscore) with optional dot-separated JSON-path segments.`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const baseIsMetaData = snakeBaseFieldName === "_meta_data" || snakeBaseFieldName === "meta_data";
|
|
151
|
+
const metaPathTargetsSensitive = baseIsMetaData && !!jsonPath && jsonPath.split(".").filter(Boolean).some(
|
|
152
|
+
(segment) => sensitiveFieldNames.has(segment) || sensitiveFieldNames.has(toSnakeCase(segment))
|
|
153
|
+
);
|
|
154
|
+
if (sensitiveFieldNames.has(snakeBaseFieldName) || metaPathTargetsSensitive) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Invalid WHERE clause field: '${fieldName}'. Filtering on sensitive fields is not allowed.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const effectiveOperator = operator === "=" && Array.isArray(value) ? "in" : operator;
|
|
160
|
+
if (!VALID_OPERATORS.includes(effectiveOperator)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Invalid WHERE clause operator: '${operator}'. Valid operators: ${VALID_OPERATORS.join(", ")}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!skipFieldValidation && !validFieldNames.has(snakeBaseFieldName)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Invalid WHERE clause field: '${fieldName}'. Field does not exist on ${itemClassName}. Valid fields: ${Array.from(validFieldNames).sort().join(", ")}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if ((effectiveOperator === "in" || effectiveOperator === "not in") && !Array.isArray(value)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`WHERE clause operator '${effectiveOperator}' requires an array value for field '${fieldName}', got ${typeof value}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if ((effectiveOperator === "in" || effectiveOperator === "not in") && Array.isArray(value) && value.length === 0) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`WHERE clause operator '${effectiveOperator}' requires a non-empty array for field '${fieldName}'. Use listByIds([]) for graceful empty array handling.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (effectiveOperator === "like" && typeof value !== "string") {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`WHERE clause operator 'like' requires a string value for field '${fieldName}', got ${typeof value}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const newKey = effectiveOperator === "=" ? snakeFieldName : `${snakeFieldName} ${effectiveOperator}`;
|
|
186
|
+
converted[newKey] = value;
|
|
187
|
+
}
|
|
188
|
+
return converted;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Gets the class constructor for items in this collection
|
|
192
|
+
*/
|
|
193
|
+
get _itemClass() {
|
|
194
|
+
const ctor = this.constructor;
|
|
195
|
+
if (!ctor._itemClass) {
|
|
196
|
+
const className = this.constructor.name;
|
|
197
|
+
const errorMessage = [
|
|
198
|
+
`Collection "${className}" must define a static _itemClass property.`,
|
|
199
|
+
"",
|
|
200
|
+
"Example:",
|
|
201
|
+
` class ${className} extends SmrtCollection<YourItemClass> {`,
|
|
202
|
+
" static readonly _itemClass = YourItemClass;",
|
|
203
|
+
" }",
|
|
204
|
+
"",
|
|
205
|
+
"Make sure your item class is imported and defined before the collection class."
|
|
206
|
+
].join("\n");
|
|
207
|
+
throw new Error(errorMessage);
|
|
208
|
+
}
|
|
209
|
+
return ctor._itemClass;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Static reference to the item class constructor
|
|
213
|
+
*/
|
|
214
|
+
static _itemClass;
|
|
215
|
+
/**
|
|
216
|
+
* Validates that the collection is properly configured
|
|
217
|
+
* Call this during development to catch configuration issues early
|
|
218
|
+
*/
|
|
219
|
+
static validate() {
|
|
220
|
+
if (!SmrtCollection._itemClass) {
|
|
221
|
+
const className = SmrtCollection.name;
|
|
222
|
+
const errorMessage = [
|
|
223
|
+
`Collection "${className}" is missing required static _itemClass property.`,
|
|
224
|
+
"",
|
|
225
|
+
"Fix by adding:",
|
|
226
|
+
` class ${className} extends SmrtCollection<YourItemClass> {`,
|
|
227
|
+
" static readonly _itemClass = YourItemClass;",
|
|
228
|
+
" }"
|
|
229
|
+
].join("\n");
|
|
230
|
+
throw new Error(errorMessage);
|
|
231
|
+
}
|
|
232
|
+
if (typeof SmrtCollection._itemClass !== "function") {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Collection "${SmrtCollection.name}"._itemClass must be a constructor function`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const hasCreateMethod = typeof SmrtCollection._itemClass.create === "function" || typeof SmrtCollection._itemClass.prototype?.create === "function";
|
|
238
|
+
if (!hasCreateMethod) {
|
|
239
|
+
logger.warn(
|
|
240
|
+
`Collection "${SmrtCollection.name}"._itemClass should have a create() method for optimal functionality`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Database table name for this collection
|
|
246
|
+
*/
|
|
247
|
+
_tableName;
|
|
248
|
+
/**
|
|
249
|
+
* Creates a new SmrtCollection instance
|
|
250
|
+
*
|
|
251
|
+
* @deprecated Use the static create() factory method instead
|
|
252
|
+
* @param options - Configuration options
|
|
253
|
+
*/
|
|
254
|
+
constructor(options = {}) {
|
|
255
|
+
super(options);
|
|
256
|
+
if (this.constructor !== SmrtCollection && this.constructor._itemClass) {
|
|
257
|
+
const itemClass = this.constructor._itemClass;
|
|
258
|
+
const itemClassName = ObjectRegistry.getClassByConstructor(itemClass)?.name || itemClass.name;
|
|
259
|
+
ObjectRegistry.registerCollection(itemClassName, this.constructor);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Factory method — the recommended way to instantiate a collection.
|
|
264
|
+
*
|
|
265
|
+
* Creates the collection instance, calls `initialize()`, and pre-populates
|
|
266
|
+
* the field cache used by synchronous query helpers.
|
|
267
|
+
*
|
|
268
|
+
* Pass the same `options` object you received in a `SmrtObject` constructor
|
|
269
|
+
* or a `SvelteKit` load function to share the database connection.
|
|
270
|
+
*
|
|
271
|
+
* @param options - Database, AI, filesystem, and other configuration options.
|
|
272
|
+
* At minimum, provide `db` (or `persistence`) to connect to a database.
|
|
273
|
+
* @returns A fully initialized, ready-to-query collection instance
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* // Share a DB connection from a SvelteKit load function
|
|
278
|
+
* const products = await Products.create(event.locals.smrtOptions);
|
|
279
|
+
*
|
|
280
|
+
* // Explicit configuration
|
|
281
|
+
* const products = await Products.create({
|
|
282
|
+
* db: myDb,
|
|
283
|
+
* ai: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY },
|
|
284
|
+
* });
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
static async create(options = {}) {
|
|
288
|
+
const {
|
|
289
|
+
_className,
|
|
290
|
+
db,
|
|
291
|
+
persistence,
|
|
292
|
+
// Also extract persistence alias
|
|
293
|
+
ai,
|
|
294
|
+
fs,
|
|
295
|
+
logging,
|
|
296
|
+
metrics,
|
|
297
|
+
pubsub,
|
|
298
|
+
sanitization,
|
|
299
|
+
signals
|
|
300
|
+
} = options;
|
|
301
|
+
const collectionOptions = {
|
|
302
|
+
_className,
|
|
303
|
+
db,
|
|
304
|
+
persistence,
|
|
305
|
+
// Pass persistence through so initialize() can map it to db
|
|
306
|
+
ai,
|
|
307
|
+
fs,
|
|
308
|
+
logging,
|
|
309
|
+
metrics,
|
|
310
|
+
pubsub,
|
|
311
|
+
sanitization,
|
|
312
|
+
signals
|
|
313
|
+
};
|
|
314
|
+
if (this._isJunctionBase === true) {
|
|
315
|
+
const itemCtor = this._itemClass;
|
|
316
|
+
const itemRegistered = itemCtor && ObjectRegistry.getClassByConstructor(itemCtor);
|
|
317
|
+
if (!itemRegistered) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`SmrtJunction subclass "${this.name}" has no registered item class. The scanner likely didn't pick up its model — usually because the consuming package's manifest doesn't include "${itemCtor?.name ?? "<unknown>"}". Check that the scanner's FRAMEWORK_BASE_CLASSES (packages/scanner/src/inheritance-resolver.ts) recognizes every framework abstract base in your inheritance chain, that the package has been built (manifest.json present in dist/), and that runtime manifest loading (__smrt-register__.ts) runs before any junction is instantiated.`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const instance = new this(collectionOptions);
|
|
324
|
+
await instance.initialize();
|
|
325
|
+
const fields = await instance.getFields();
|
|
326
|
+
instance._cachedFields = fields;
|
|
327
|
+
return instance;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Async initialization hook for the collection.
|
|
331
|
+
*
|
|
332
|
+
* Called automatically by the static `create()` factory. In most cases you
|
|
333
|
+
* do not need to call this directly — use `create()` instead.
|
|
334
|
+
*
|
|
335
|
+
* Runtime schema checks are deferred until a query actually touches the
|
|
336
|
+
* backing table. This keeps collection construction safe during SSR and
|
|
337
|
+
* import-time module evaluation without mutating application schema.
|
|
338
|
+
*
|
|
339
|
+
* @returns This instance (enables chaining)
|
|
340
|
+
*/
|
|
341
|
+
async initialize() {
|
|
342
|
+
await super.initialize();
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Verify that the collection's backing table exists before running a query.
|
|
347
|
+
*
|
|
348
|
+
* This is a fail-fast check only. It does not create or alter schema.
|
|
349
|
+
*/
|
|
350
|
+
async ensureStorageReady() {
|
|
351
|
+
await verifyPersistenceTable(
|
|
352
|
+
this.db,
|
|
353
|
+
this.tableName,
|
|
354
|
+
this.getResolvedItemClassName()
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Find a single record by criteria (convenience method - delegates to get())
|
|
359
|
+
*
|
|
360
|
+
* @param options - Query options with where clause
|
|
361
|
+
* @returns Promise resolving to the object or null if not found
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* const councils = await Councils.create({ persistence: { type: 'sql', url: 'db.sqlite' } });
|
|
366
|
+
* const council = await councils.findOne({ where: { name: 'Example Council' } });
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
async findOne(options) {
|
|
370
|
+
return await this.get(options.where);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Find a record by ID (convenience method - delegates to get())
|
|
374
|
+
*
|
|
375
|
+
* @param id - Record ID to find (string or Field instance)
|
|
376
|
+
* @returns Promise resolving to the object or null if not found
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* const councils = await Councils.create({ persistence: { type: 'sql', url: 'db.sqlite' } });
|
|
381
|
+
* const council = await councils.findById('uuid-123');
|
|
382
|
+
*
|
|
383
|
+
* // Also works with Field instances (e.g., from foreignKey fields)
|
|
384
|
+
* const meeting = new Meeting({ councilId: 'uuid-123' });
|
|
385
|
+
* const council = await councils.findById(meeting.councilId);
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
async findById(id) {
|
|
389
|
+
return await this.get(id);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Find all records matching criteria (convenience method - delegates to list())
|
|
393
|
+
*
|
|
394
|
+
* @param options - Query options (where, orderBy, limit, etc.)
|
|
395
|
+
* @returns Promise resolving to array of objects
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* const councils = await Councils.create({ persistence: { type: 'sql', url: 'db.sqlite' } });
|
|
400
|
+
* const active = await councils.findAll({ where: { status: 'active' } });
|
|
401
|
+
* ```
|
|
402
|
+
*/
|
|
403
|
+
async findAll(options = {}) {
|
|
404
|
+
return await this.list(options);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Find multiple objects by their IDs in a single query.
|
|
408
|
+
*
|
|
409
|
+
* This is a convenience method that avoids N+1 queries when you have
|
|
410
|
+
* a list of IDs and need to fetch the corresponding records.
|
|
411
|
+
*
|
|
412
|
+
* @param ids - Array of UUIDs to fetch
|
|
413
|
+
* @returns Promise resolving to array of objects (order not guaranteed)
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* const profiles = await profileCollection.listByIds(['id1', 'id2', 'id3']);
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
async listByIds(ids) {
|
|
421
|
+
if (ids.length === 0) return [];
|
|
422
|
+
return this.list({ where: { id: ids } });
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve the effective cache config for a read (issue #1498).
|
|
426
|
+
*
|
|
427
|
+
* Per-call options win: `false` forces a fresh read, `{ ttl }` enables
|
|
428
|
+
* caching for this call. Otherwise falls back to the model-level
|
|
429
|
+
* `@smrt({ cache })` config (inheritance-aware). Returns undefined when
|
|
430
|
+
* the read should go straight to the database — the default.
|
|
431
|
+
*/
|
|
432
|
+
resolveReadCacheConfig(perCall) {
|
|
433
|
+
if (perCall === false) return void 0;
|
|
434
|
+
if (perCall) return perCall.ttl > 0 ? perCall : void 0;
|
|
435
|
+
return ObjectRegistry.resolveCollectionCacheConfig(
|
|
436
|
+
this.getResolvedItemQualifiedName()
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Execute a SELECT, optionally through the collection read cache.
|
|
441
|
+
*
|
|
442
|
+
* The cache key is the final SQL + bound parameters — computed after
|
|
443
|
+
* interceptors (tenancy filters) and STI discriminators are applied, so
|
|
444
|
+
* differently-scoped queries can never share an entry. Cached values are
|
|
445
|
+
* raw rows; hydration and read interceptors still run on every call.
|
|
446
|
+
*/
|
|
447
|
+
async queryRowsWithCache(sql, params, cacheConfig) {
|
|
448
|
+
if (!cacheConfig) {
|
|
449
|
+
const result2 = await this.db.query(sql, ...params);
|
|
450
|
+
return result2.rows;
|
|
451
|
+
}
|
|
452
|
+
const dbKey = resolveDbCacheKey(this.db);
|
|
453
|
+
const queryKey = buildQueryCacheKey(sql, params);
|
|
454
|
+
if (cacheConfig.crossProcess) {
|
|
455
|
+
ensureCacheInvalidationListener(this.db);
|
|
456
|
+
registerCrossProcessCacheInterest(dbKey, this.tableName);
|
|
457
|
+
}
|
|
458
|
+
const cached = getCachedRows(dbKey, this.tableName, queryKey);
|
|
459
|
+
if (cached) {
|
|
460
|
+
return cached;
|
|
461
|
+
}
|
|
462
|
+
const generation = getCacheGeneration(dbKey, this.tableName);
|
|
463
|
+
const result = await this.db.query(sql, ...params);
|
|
464
|
+
setCachedRows(
|
|
465
|
+
dbKey,
|
|
466
|
+
this.tableName,
|
|
467
|
+
queryKey,
|
|
468
|
+
result.rows,
|
|
469
|
+
cacheConfig.ttl,
|
|
470
|
+
generation
|
|
471
|
+
);
|
|
472
|
+
return result.rows;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Retrieves a single object from the collection by ID, slug, or a custom filter.
|
|
476
|
+
*
|
|
477
|
+
* Filter resolution:
|
|
478
|
+
* - UUID string → `WHERE id = ?`
|
|
479
|
+
* - Non-UUID string → `WHERE slug = ? AND context = ''`
|
|
480
|
+
* - Object → `WHERE <key> = <value> [AND ...]`
|
|
481
|
+
*
|
|
482
|
+
* For STI child collections, an `AND _meta_type = '<qualifiedName>'` clause is
|
|
483
|
+
* automatically appended so you only receive the correct subclass.
|
|
484
|
+
*
|
|
485
|
+
* Runs `beforeGet` / `afterGet` interceptors (used by multi-tenancy, etc.).
|
|
486
|
+
*
|
|
487
|
+
* @param filter - UUID string, slug string, or a WHERE conditions object
|
|
488
|
+
* @param options.cache - Opt-in read-through cache for this call
|
|
489
|
+
* (`{ ttl }` in milliseconds), or `false` to force a fresh read when the
|
|
490
|
+
* model opted in via `@smrt({ cache })`. Defaults to the model config.
|
|
491
|
+
* @returns The matching object instance, or `null` if not found
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```typescript
|
|
495
|
+
* // By UUID
|
|
496
|
+
* const product = await products.get('550e8400-e29b-41d4-a716-446655440000');
|
|
497
|
+
*
|
|
498
|
+
* // By slug
|
|
499
|
+
* const product = await products.get('my-widget');
|
|
500
|
+
*
|
|
501
|
+
* // By custom filter
|
|
502
|
+
* const product = await products.get({ sku: 'WID-001' });
|
|
503
|
+
*
|
|
504
|
+
* // Cached read (memoized for 60s, invalidated on writes)
|
|
505
|
+
* const product = await products.get('my-widget', { cache: { ttl: 60_000 } });
|
|
506
|
+
* ```
|
|
507
|
+
*
|
|
508
|
+
* @see {@link list} for multiple results
|
|
509
|
+
* @see {@link findById} / {@link findOne} for convenience aliases
|
|
510
|
+
*/
|
|
511
|
+
async get(filter, options = {}) {
|
|
512
|
+
await this.ensureStorageReady();
|
|
513
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
514
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
515
|
+
const interceptorContext = createInterceptorContext(
|
|
516
|
+
itemClassName,
|
|
517
|
+
"get",
|
|
518
|
+
this.constructor.name
|
|
519
|
+
);
|
|
520
|
+
const interceptedFilter = await GlobalInterceptors.executeBeforeGet(
|
|
521
|
+
itemClassName,
|
|
522
|
+
filter,
|
|
523
|
+
interceptorContext
|
|
524
|
+
);
|
|
525
|
+
let where = typeof interceptedFilter === "string" ? /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
526
|
+
interceptedFilter
|
|
527
|
+
) ? { id: interceptedFilter } : { slug: interceptedFilter, context: "" } : interceptedFilter;
|
|
528
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(itemQualifiedName);
|
|
529
|
+
const isSTI = tableStrategy === "sti";
|
|
530
|
+
if (isSTI) {
|
|
531
|
+
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
532
|
+
if (stiBase && stiBase !== itemQualifiedName) {
|
|
533
|
+
where = {
|
|
534
|
+
_meta_type: itemQualifiedName,
|
|
535
|
+
...where
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const convertedWhere = this.convertWhereKeys(where);
|
|
540
|
+
const { sql: whereSql, values: whereValues } = buildWhere(convertedWhere);
|
|
541
|
+
const fullSQL = `SELECT * FROM ${this.tableName} ${whereSql}`;
|
|
542
|
+
const rows = await this.queryRowsWithCache(
|
|
543
|
+
fullSQL,
|
|
544
|
+
whereValues,
|
|
545
|
+
this.resolveReadCacheConfig(options.cache)
|
|
546
|
+
);
|
|
547
|
+
if (!rows?.[0]) {
|
|
548
|
+
return await GlobalInterceptors.executeAfterGet(
|
|
549
|
+
itemClassName,
|
|
550
|
+
null,
|
|
551
|
+
interceptorContext
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const fields = this.getFieldsSync();
|
|
555
|
+
const instance = await this.hydrateResultRow(rows[0], fields, isSTI);
|
|
556
|
+
return await GlobalInterceptors.executeAfterGet(
|
|
557
|
+
itemClassName,
|
|
558
|
+
instance,
|
|
559
|
+
interceptorContext
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Lists records from the collection with flexible filtering options
|
|
564
|
+
*
|
|
565
|
+
* @param options - Query options object
|
|
566
|
+
* @param options.where - Record of conditions to filter results. Each key can include an operator
|
|
567
|
+
* separated by a space (e.g., 'price >', 'name like'). Default operator is '='.
|
|
568
|
+
* @param options.offset - Number of records to skip
|
|
569
|
+
* @param options.limit - Maximum number of records to return
|
|
570
|
+
* @param options.orderBy - Field(s) to order results by, with optional direction
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```typescript
|
|
574
|
+
* // Find active products priced between $100-$200
|
|
575
|
+
* await collection.list({
|
|
576
|
+
* where: {
|
|
577
|
+
* 'price >': 100,
|
|
578
|
+
* 'price <=': 200,
|
|
579
|
+
* 'status': 'active', // equals operator is default
|
|
580
|
+
* 'category in': ['A', 'B', 'C'], // IN operator for arrays
|
|
581
|
+
* 'name like': '%shirt%', // LIKE for pattern matching
|
|
582
|
+
* 'deleted_at !=': null // exclude deleted items
|
|
583
|
+
* },
|
|
584
|
+
* limit: 10,
|
|
585
|
+
* offset: 0
|
|
586
|
+
* });
|
|
587
|
+
*
|
|
588
|
+
* // Find users matching pattern but not in specific roles
|
|
589
|
+
* await users.list({
|
|
590
|
+
* where: {
|
|
591
|
+
* 'email like': '%@company.com',
|
|
592
|
+
* 'active': true,
|
|
593
|
+
* 'role in': ['guest', 'blocked'],
|
|
594
|
+
* 'last_login <': lastMonth
|
|
595
|
+
* }
|
|
596
|
+
* });
|
|
597
|
+
* ```
|
|
598
|
+
*
|
|
599
|
+
* @returns Promise resolving to an array of model instances
|
|
600
|
+
*/
|
|
601
|
+
async list(options = {}) {
|
|
602
|
+
await this.ensureStorageReady();
|
|
603
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
604
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
605
|
+
const interceptorContext = createInterceptorContext(
|
|
606
|
+
itemClassName,
|
|
607
|
+
"list",
|
|
608
|
+
this.constructor.name
|
|
609
|
+
);
|
|
610
|
+
const interceptedOptions = await GlobalInterceptors.executeBeforeList(
|
|
611
|
+
itemClassName,
|
|
612
|
+
options,
|
|
613
|
+
interceptorContext
|
|
614
|
+
) ?? options ?? {};
|
|
615
|
+
let { where, offset, limit, orderBy } = interceptedOptions;
|
|
616
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(itemQualifiedName);
|
|
617
|
+
const isSTI = tableStrategy === "sti";
|
|
618
|
+
if (isSTI) {
|
|
619
|
+
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
620
|
+
if (stiBase && stiBase !== itemQualifiedName) {
|
|
621
|
+
where = {
|
|
622
|
+
_meta_type: itemQualifiedName,
|
|
623
|
+
...where || {}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
where = resolveMetaTypeInWhere(where);
|
|
628
|
+
const convertedWhere = this.convertWhereKeys(where || {});
|
|
629
|
+
const { sql: whereSql, values: whereValues } = buildWhere(convertedWhere);
|
|
630
|
+
let orderBySql = "";
|
|
631
|
+
if (orderBy) {
|
|
632
|
+
orderBySql = " ORDER BY ";
|
|
633
|
+
const orderByItems = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
634
|
+
orderBySql += orderByItems.map((item) => {
|
|
635
|
+
const [field, direction = "ASC"] = item.split(" ");
|
|
636
|
+
if (!/^[a-zA-Z0-9_]+$/.test(field)) {
|
|
637
|
+
throw new Error(`Invalid field name for ordering: ${field}`);
|
|
638
|
+
}
|
|
639
|
+
const normalizedDirection = direction.toUpperCase();
|
|
640
|
+
if (normalizedDirection !== "ASC" && normalizedDirection !== "DESC") {
|
|
641
|
+
throw new Error(
|
|
642
|
+
`Invalid sort direction: ${direction}. Must be ASC or DESC.`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
const snakeField = toSnakeCase(field);
|
|
646
|
+
return `${snakeField} ${normalizedDirection}`;
|
|
647
|
+
}).join(", ");
|
|
648
|
+
}
|
|
649
|
+
let limitOffsetSql = "";
|
|
650
|
+
const limitOffsetValues = [];
|
|
651
|
+
let paramIndex = whereValues.length + 1;
|
|
652
|
+
if (limit !== void 0) {
|
|
653
|
+
limitOffsetSql += ` LIMIT $${paramIndex++}`;
|
|
654
|
+
limitOffsetValues.push(limit);
|
|
655
|
+
}
|
|
656
|
+
if (offset !== void 0) {
|
|
657
|
+
limitOffsetSql += ` OFFSET $${paramIndex++}`;
|
|
658
|
+
limitOffsetValues.push(offset);
|
|
659
|
+
}
|
|
660
|
+
const sql = `SELECT * FROM ${this.tableName} ${whereSql} ${orderBySql} ${limitOffsetSql}`;
|
|
661
|
+
const params = [...whereValues, ...limitOffsetValues];
|
|
662
|
+
const rows = await this.queryRowsWithCache(
|
|
663
|
+
sql,
|
|
664
|
+
params,
|
|
665
|
+
this.resolveReadCacheConfig(
|
|
666
|
+
interceptedOptions.cache
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
const fields = this.getFieldsSync();
|
|
670
|
+
const instances = await Promise.all(
|
|
671
|
+
rows.map((item) => this.hydrateResultRow(item, fields, isSTI))
|
|
672
|
+
);
|
|
673
|
+
if (interceptedOptions.include && interceptedOptions.include.length > 0) {
|
|
674
|
+
await this.eagerLoadRelationships(
|
|
675
|
+
instances,
|
|
676
|
+
interceptedOptions.include
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
const finalInstances = await GlobalInterceptors.executeAfterList(
|
|
680
|
+
itemClassName,
|
|
681
|
+
instances,
|
|
682
|
+
interceptorContext
|
|
683
|
+
);
|
|
684
|
+
return finalInstances;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Eagerly load relationships for a collection of instances
|
|
688
|
+
*
|
|
689
|
+
* Optimizes loading by batching queries for foreignKey relationships to avoid N+1 queries.
|
|
690
|
+
*
|
|
691
|
+
* @param instances - Array of object instances to load relationships for
|
|
692
|
+
* @param relationships - Array of relationship field names to load
|
|
693
|
+
* @private
|
|
694
|
+
*/
|
|
695
|
+
async eagerLoadRelationships(instances, relationships) {
|
|
696
|
+
if (instances.length === 0) return;
|
|
697
|
+
for (const fieldName of relationships) {
|
|
698
|
+
const relationshipMeta = ObjectRegistry.getRelationships(
|
|
699
|
+
this._itemClass.name
|
|
700
|
+
);
|
|
701
|
+
const relationship = relationshipMeta.find(
|
|
702
|
+
(r) => r.fieldName === fieldName
|
|
703
|
+
);
|
|
704
|
+
if (!relationship) {
|
|
705
|
+
logger.warn(
|
|
706
|
+
`Relationship ${fieldName} not found on ${this._itemClass.name}, skipping eager load`
|
|
707
|
+
);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (relationship.type === "foreignKey" || relationship.type === "crossPackageRef") {
|
|
711
|
+
if (relationship.type === "crossPackageRef") {
|
|
712
|
+
await ObjectRegistry.ensureManifestLoaded(relationship.targetClass);
|
|
713
|
+
}
|
|
714
|
+
await this.batchLoadForeignKeys(instances, fieldName, relationship);
|
|
715
|
+
} else if (relationship.type === "oneToMany") {
|
|
716
|
+
await this.batchLoadOneToMany(instances, fieldName, relationship);
|
|
717
|
+
} else if (relationship.type === "manyToMany") {
|
|
718
|
+
await this.batchLoadManyToMany(instances, fieldName, relationship);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Batch load foreignKey relationships to avoid N+1 queries
|
|
724
|
+
*
|
|
725
|
+
* @param instances - Instances to load relationships for
|
|
726
|
+
* @param fieldName - Name of the foreignKey field
|
|
727
|
+
* @param relationship - Relationship metadata
|
|
728
|
+
* @private
|
|
729
|
+
*/
|
|
730
|
+
async batchLoadForeignKeys(instances, fieldName, relationship) {
|
|
731
|
+
const foreignKeyValues = /* @__PURE__ */ new Set();
|
|
732
|
+
for (const instance of instances) {
|
|
733
|
+
const value = instance[fieldName];
|
|
734
|
+
if (value && typeof value === "string") {
|
|
735
|
+
foreignKeyValues.add(value);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (foreignKeyValues.size === 0) return;
|
|
739
|
+
let targetCollection;
|
|
740
|
+
try {
|
|
741
|
+
targetCollection = await ObjectRegistry.getCollection(
|
|
742
|
+
relationship.targetClass,
|
|
743
|
+
this.options
|
|
744
|
+
);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
logger.warn(`Could not get collection for ${relationship.targetClass}`, {
|
|
747
|
+
error
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const fkValueList = Array.from(foreignKeyValues);
|
|
752
|
+
const relatedObjects = [];
|
|
753
|
+
for (const idChunk of chunkArray(fkValueList, IN_LIST_CHUNK_SIZE)) {
|
|
754
|
+
const batch = await targetCollection.list({
|
|
755
|
+
where: { "id in": idChunk }
|
|
756
|
+
});
|
|
757
|
+
relatedObjects.push(...batch);
|
|
758
|
+
}
|
|
759
|
+
const relatedMap = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const obj of relatedObjects) {
|
|
761
|
+
relatedMap.set(obj.id, obj);
|
|
762
|
+
}
|
|
763
|
+
for (const instance of instances) {
|
|
764
|
+
const foreignKeyValue = instance[fieldName];
|
|
765
|
+
if (foreignKeyValue && typeof foreignKeyValue === "string") {
|
|
766
|
+
const relatedObject = relatedMap.get(foreignKeyValue);
|
|
767
|
+
if (relatedObject) {
|
|
768
|
+
instance._loadedRelationships.set(fieldName, relatedObject);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Batch load oneToMany relationships
|
|
775
|
+
*
|
|
776
|
+
* @param instances - Instances to load relationships for
|
|
777
|
+
* @param fieldName - Name of the oneToMany field
|
|
778
|
+
* @param relationship - Relationship metadata
|
|
779
|
+
* @private
|
|
780
|
+
*/
|
|
781
|
+
async batchLoadOneToMany(instances, fieldName, relationship) {
|
|
782
|
+
const inverseRelationships = ObjectRegistry.getInverseRelationshipsForSelf(
|
|
783
|
+
this._itemClass.name
|
|
784
|
+
);
|
|
785
|
+
const inverseCandidates = inverseRelationships.filter(
|
|
786
|
+
(r) => r.sourceClass === relationship.targetClass && r.type === "foreignKey"
|
|
787
|
+
);
|
|
788
|
+
const explicitForeignKey = relationship.options?.foreignKey;
|
|
789
|
+
const matchedForeignKey = explicitForeignKey ? inverseCandidates.find((r) => r.fieldName === explicitForeignKey) : void 0;
|
|
790
|
+
if (explicitForeignKey && !matchedForeignKey) {
|
|
791
|
+
throw new Error(
|
|
792
|
+
`oneToMany ${fieldName} specifies foreignKey '${explicitForeignKey}', but ${relationship.targetClass} has no matching inverse foreignKey. Candidates: ${inverseCandidates.map((r) => r.fieldName).join(", ") || "(none)"}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const inverseForeignKey = matchedForeignKey ?? inverseCandidates.find((r) => r.targetClass === this._itemClass.name) ?? inverseCandidates[0];
|
|
796
|
+
if (!inverseForeignKey) {
|
|
797
|
+
logger.warn(
|
|
798
|
+
`Could not find inverse foreignKey for oneToMany ${fieldName}`
|
|
799
|
+
);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const instanceIds = instances.map((i) => i.id).filter((id) => !!id);
|
|
803
|
+
if (instanceIds.length === 0) return;
|
|
804
|
+
let targetCollection;
|
|
805
|
+
try {
|
|
806
|
+
targetCollection = await ObjectRegistry.getCollection(
|
|
807
|
+
relationship.targetClass,
|
|
808
|
+
this.options
|
|
809
|
+
);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
logger.warn(`Could not get collection for ${relationship.targetClass}`, {
|
|
812
|
+
error
|
|
813
|
+
});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const relatedObjects = [];
|
|
817
|
+
for (const idChunk of chunkArray(instanceIds, IN_LIST_CHUNK_SIZE)) {
|
|
818
|
+
const batch = await targetCollection.list({
|
|
819
|
+
where: { [`${inverseForeignKey.fieldName} in`]: idChunk }
|
|
820
|
+
});
|
|
821
|
+
relatedObjects.push(...batch);
|
|
822
|
+
}
|
|
823
|
+
const relatedMap = /* @__PURE__ */ new Map();
|
|
824
|
+
for (const obj of relatedObjects) {
|
|
825
|
+
const foreignKeyValue = obj[inverseForeignKey.fieldName];
|
|
826
|
+
if (!relatedMap.has(foreignKeyValue)) {
|
|
827
|
+
relatedMap.set(foreignKeyValue, []);
|
|
828
|
+
}
|
|
829
|
+
relatedMap.get(foreignKeyValue)?.push(obj);
|
|
830
|
+
}
|
|
831
|
+
for (const instance of instances) {
|
|
832
|
+
const relatedArray = relatedMap.get(instance.id) || [];
|
|
833
|
+
instance._loadedRelationships.set(fieldName, relatedArray);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Batch-load manyToMany relationships through a junction table.
|
|
838
|
+
*
|
|
839
|
+
* Issues two queries instead of N: one against the junction table to map
|
|
840
|
+
* source IDs to target IDs, and one against the target table to hydrate
|
|
841
|
+
* the related rows. Results are grouped by source instance.
|
|
842
|
+
*
|
|
843
|
+
* @param instances - Instances whose manyToMany field should be populated
|
|
844
|
+
* @param fieldName - Name of the @manyToMany decorated field
|
|
845
|
+
* @param relationship - Relationship metadata from the registry
|
|
846
|
+
* @private
|
|
847
|
+
*/
|
|
848
|
+
async batchLoadManyToMany(instances, fieldName, relationship) {
|
|
849
|
+
const instanceIds = instances.map((i) => i.id).filter((id) => !!id);
|
|
850
|
+
if (instanceIds.length === 0) return;
|
|
851
|
+
let through;
|
|
852
|
+
let sourceColumn;
|
|
853
|
+
let targetColumn;
|
|
854
|
+
let targetClassName;
|
|
855
|
+
try {
|
|
856
|
+
const sample = instances[0];
|
|
857
|
+
const join = await sample.resolveManyToManyJoin(fieldName, relationship);
|
|
858
|
+
through = join.through;
|
|
859
|
+
sourceColumn = join.sourceColumn;
|
|
860
|
+
targetColumn = join.targetColumn;
|
|
861
|
+
targetClassName = join.targetClassName;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
logger.warn(
|
|
864
|
+
`Could not resolve manyToMany join for ${fieldName} on ${this._itemClass.name}`,
|
|
865
|
+
{ error }
|
|
866
|
+
);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
for (const instance of instances) {
|
|
870
|
+
instance._loadedRelationships.set(fieldName, []);
|
|
871
|
+
}
|
|
872
|
+
const junctionRowsAll = [];
|
|
873
|
+
for (const idChunk of chunkArray(instanceIds, IN_LIST_CHUNK_SIZE)) {
|
|
874
|
+
const placeholders = idChunk.map(() => "?").join(", ");
|
|
875
|
+
const result = await this.db.query(
|
|
876
|
+
`SELECT "${sourceColumn}", "${targetColumn}" FROM "${through}" WHERE "${sourceColumn}" IN (${placeholders})`,
|
|
877
|
+
idChunk
|
|
878
|
+
);
|
|
879
|
+
junctionRowsAll.push(...result.rows);
|
|
880
|
+
}
|
|
881
|
+
if (junctionRowsAll.length === 0) return;
|
|
882
|
+
const sourceToTargets = /* @__PURE__ */ new Map();
|
|
883
|
+
const allTargetIds = /* @__PURE__ */ new Set();
|
|
884
|
+
for (const row of junctionRowsAll) {
|
|
885
|
+
const sId = row[sourceColumn];
|
|
886
|
+
const tId = row[targetColumn];
|
|
887
|
+
if (typeof sId !== "string" || typeof tId !== "string") continue;
|
|
888
|
+
allTargetIds.add(tId);
|
|
889
|
+
const list = sourceToTargets.get(sId) ?? [];
|
|
890
|
+
list.push(tId);
|
|
891
|
+
sourceToTargets.set(sId, list);
|
|
892
|
+
}
|
|
893
|
+
if (allTargetIds.size === 0) return;
|
|
894
|
+
let targetCollection;
|
|
895
|
+
try {
|
|
896
|
+
targetCollection = await ObjectRegistry.getCollection(
|
|
897
|
+
targetClassName,
|
|
898
|
+
this.options
|
|
899
|
+
);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
logger.warn(
|
|
902
|
+
`Could not get collection for manyToMany target ${targetClassName}`,
|
|
903
|
+
{ error }
|
|
904
|
+
);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const targetIdList = Array.from(allTargetIds);
|
|
908
|
+
const targetObjects = [];
|
|
909
|
+
for (const idChunk of chunkArray(targetIdList, IN_LIST_CHUNK_SIZE)) {
|
|
910
|
+
const batch = await targetCollection.list({
|
|
911
|
+
where: { "id in": idChunk }
|
|
912
|
+
});
|
|
913
|
+
targetObjects.push(...batch);
|
|
914
|
+
}
|
|
915
|
+
const targetById = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const obj of targetObjects) {
|
|
917
|
+
if (obj.id) targetById.set(obj.id, obj);
|
|
918
|
+
}
|
|
919
|
+
for (const instance of instances) {
|
|
920
|
+
const targetIds = sourceToTargets.get(instance.id) ?? [];
|
|
921
|
+
const objects = targetIds.map((id) => targetById.get(id)).filter((o) => o !== void 0);
|
|
922
|
+
instance._loadedRelationships.set(fieldName, objects);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Creates and persists a new instance of the collection's item class.
|
|
927
|
+
*
|
|
928
|
+
* Instantiates the model with the given field values, calls `initialize()`,
|
|
929
|
+
* assigns a UUID if none is provided, then calls `save()` to write the row
|
|
930
|
+
* to the database.
|
|
931
|
+
*
|
|
932
|
+
* For STI collections, pass `_meta_type` to create a specific subclass.
|
|
933
|
+
* The correct constructor is resolved via `ObjectRegistry` (polymorphic creation).
|
|
934
|
+
*
|
|
935
|
+
* @param options - Field values for the new object. Accepts any public field on
|
|
936
|
+
* the model class plus the STI `_meta_type` discriminator.
|
|
937
|
+
* @returns The newly created and saved model instance
|
|
938
|
+
* @throws {ValidationError} If a `required` field is missing or a unique constraint is violated
|
|
939
|
+
* @throws {DatabaseError} If the write fails
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```typescript
|
|
943
|
+
* // Regular creation
|
|
944
|
+
* const product = await products.create({ name: 'Widget', price: 9.99 });
|
|
945
|
+
* console.log(product.id); // UUID assigned during save
|
|
946
|
+
*
|
|
947
|
+
* // STI polymorphic creation
|
|
948
|
+
* const article = await contents.create({
|
|
949
|
+
* _meta_type: '@happyvertical/smrt-content:Article',
|
|
950
|
+
* title: 'Hello World',
|
|
951
|
+
* });
|
|
952
|
+
* ```
|
|
953
|
+
*
|
|
954
|
+
* @see {@link getOrUpsert} to avoid duplicates by finding-or-creating
|
|
955
|
+
*/
|
|
956
|
+
async create(options) {
|
|
957
|
+
let itemClassName = this.getResolvedItemClassName();
|
|
958
|
+
await ObjectRegistry.ensureManifestLoaded(itemClassName);
|
|
959
|
+
itemClassName = this.getResolvedItemClassName();
|
|
960
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
961
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(itemQualifiedName);
|
|
962
|
+
if (tableStrategy === "sti" && options._meta_type) {
|
|
963
|
+
const instance2 = await this.createPolymorphic(
|
|
964
|
+
options._meta_type,
|
|
965
|
+
options
|
|
966
|
+
);
|
|
967
|
+
if (!instance2.id) {
|
|
968
|
+
instance2._id = crypto.randomUUID();
|
|
969
|
+
}
|
|
970
|
+
await instance2.save();
|
|
971
|
+
return instance2;
|
|
972
|
+
}
|
|
973
|
+
const params = {
|
|
974
|
+
ai: this.options.ai,
|
|
975
|
+
// Pass the actual database instance, not options
|
|
976
|
+
// This ensures objects share the same connection as the collection
|
|
977
|
+
// Critical for in-memory databases like DuckDB :memory: where each
|
|
978
|
+
// connection gets a separate database
|
|
979
|
+
db: this.db,
|
|
980
|
+
_skipLoad: true,
|
|
981
|
+
// Don't try to load from DB - this is a new object
|
|
982
|
+
...options
|
|
983
|
+
};
|
|
984
|
+
const instance = new this._itemClass(params);
|
|
985
|
+
await instance.initialize();
|
|
986
|
+
if (tableStrategy === "sti") {
|
|
987
|
+
instance._meta_type = itemQualifiedName;
|
|
988
|
+
}
|
|
989
|
+
if (!instance.id) {
|
|
990
|
+
instance._id = crypto.randomUUID();
|
|
991
|
+
}
|
|
992
|
+
await instance.save();
|
|
993
|
+
return instance;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Creates an instance of the correct subclass for STI polymorphic queries
|
|
997
|
+
*
|
|
998
|
+
* @param className - Name of the class to instantiate (from _meta_type)
|
|
999
|
+
* @param options - Data to initialize the instance with
|
|
1000
|
+
* @returns Promise resolving to the instance of the correct subclass
|
|
1001
|
+
* @private
|
|
1002
|
+
*/
|
|
1003
|
+
async createPolymorphic(className, options, hydrationOptions = {}) {
|
|
1004
|
+
if (!className || className === null || className === void 0) {
|
|
1005
|
+
const { DatabaseError } = await import("./errors.js");
|
|
1006
|
+
throw DatabaseError.missingDiscriminator(
|
|
1007
|
+
this._itemClass.name,
|
|
1008
|
+
options?.id
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
let registeredClass = ObjectRegistry.getClassByQualifiedName(className);
|
|
1012
|
+
if (!registeredClass) {
|
|
1013
|
+
registeredClass = ObjectRegistry.getClass(className);
|
|
1014
|
+
}
|
|
1015
|
+
if (!registeredClass) {
|
|
1016
|
+
await ObjectRegistry.ensureManifestLoaded(className);
|
|
1017
|
+
registeredClass = ObjectRegistry.getClassByQualifiedName(className);
|
|
1018
|
+
if (!registeredClass) {
|
|
1019
|
+
registeredClass = ObjectRegistry.getClass(className);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (!registeredClass) {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
`STI polymorphic query failed: Class '${className}' not found in ObjectRegistry. Ensure the class is registered with @smrt() decorator or available via an installed SMRT manifest.`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
const params = {
|
|
1028
|
+
ai: this.options.ai,
|
|
1029
|
+
db: this.db,
|
|
1030
|
+
_skipLoad: true,
|
|
1031
|
+
...hydrationOptions.hydrateOnly ? {
|
|
1032
|
+
_reuseInitializedDb: true,
|
|
1033
|
+
_deferRuntimeInitialization: true
|
|
1034
|
+
} : {},
|
|
1035
|
+
...options
|
|
1036
|
+
};
|
|
1037
|
+
const instance = new registeredClass.constructor(params);
|
|
1038
|
+
await instance.initialize();
|
|
1039
|
+
instance._meta_type = registeredClass?.qualifiedName || className;
|
|
1040
|
+
return instance;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Finds an existing record matching `data` or creates it if not found.
|
|
1044
|
+
*
|
|
1045
|
+
* Look-up priority:
|
|
1046
|
+
* 1. `data.id` — query by primary key
|
|
1047
|
+
* 2. `data.slug` — query by slug + context
|
|
1048
|
+
* 3. Fallback — query by the full `data` object as a WHERE clause
|
|
1049
|
+
*
|
|
1050
|
+
* If a matching record is found, it is updated with any changed fields from
|
|
1051
|
+
* `data` (diffed against the existing record) and saved. If no match is
|
|
1052
|
+
* found, `defaults` are merged with `data` and a new record is created.
|
|
1053
|
+
*
|
|
1054
|
+
* @param data - The field values to find or upsert
|
|
1055
|
+
* @param defaults - Extra default values applied only when creating a new record
|
|
1056
|
+
* @returns The existing (possibly updated) or newly created object instance
|
|
1057
|
+
*
|
|
1058
|
+
* @example
|
|
1059
|
+
* ```typescript
|
|
1060
|
+
* // Find-or-create a tag by slug
|
|
1061
|
+
* const tag = await tags.getOrUpsert({ slug: 'javascript', name: 'JavaScript' });
|
|
1062
|
+
*
|
|
1063
|
+
* // With defaults applied only on creation
|
|
1064
|
+
* const user = await users.getOrUpsert(
|
|
1065
|
+
* { email: 'alice@example.com' },
|
|
1066
|
+
* { role: 'member', active: true },
|
|
1067
|
+
* );
|
|
1068
|
+
* ```
|
|
1069
|
+
*
|
|
1070
|
+
* @see {@link create} for always-insert semantics
|
|
1071
|
+
* @see {@link get} for read-only lookup
|
|
1072
|
+
*/
|
|
1073
|
+
async getOrUpsert(data, defaults = {}) {
|
|
1074
|
+
const logicalData = this.normalizeLogicalData(data);
|
|
1075
|
+
const logicalDefaults = this.normalizeLogicalData(defaults);
|
|
1076
|
+
let where = {};
|
|
1077
|
+
const diffData = { ...logicalData };
|
|
1078
|
+
if (logicalData.id) {
|
|
1079
|
+
where = { id: logicalData.id };
|
|
1080
|
+
delete diffData.id;
|
|
1081
|
+
delete diffData.slug;
|
|
1082
|
+
delete diffData.context;
|
|
1083
|
+
} else if (logicalData.slug) {
|
|
1084
|
+
where = { slug: logicalData.slug, context: logicalData.context || "" };
|
|
1085
|
+
delete diffData.slug;
|
|
1086
|
+
delete diffData.context;
|
|
1087
|
+
} else {
|
|
1088
|
+
where = logicalData;
|
|
1089
|
+
}
|
|
1090
|
+
const existing = await this.get(where, { cache: false });
|
|
1091
|
+
if (existing) {
|
|
1092
|
+
const diff = this.getDiffSync(existing, diffData);
|
|
1093
|
+
if (diff) {
|
|
1094
|
+
Object.assign(existing, diff);
|
|
1095
|
+
await existing.save();
|
|
1096
|
+
}
|
|
1097
|
+
return existing;
|
|
1098
|
+
}
|
|
1099
|
+
const createData = {
|
|
1100
|
+
...logicalDefaults,
|
|
1101
|
+
...logicalData
|
|
1102
|
+
};
|
|
1103
|
+
return await this.create(createData);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Gets differences between an existing object and new data
|
|
1107
|
+
*
|
|
1108
|
+
* @param existing - Existing object
|
|
1109
|
+
* @param data - New data
|
|
1110
|
+
* @returns Object containing only the changed fields
|
|
1111
|
+
*/
|
|
1112
|
+
async getDiff(existing, data) {
|
|
1113
|
+
return this.getDiffSync(existing, data);
|
|
1114
|
+
}
|
|
1115
|
+
getDiffSync(existing, data) {
|
|
1116
|
+
const fields = this.getFieldsSync();
|
|
1117
|
+
const validKeys = /* @__PURE__ */ new Set([
|
|
1118
|
+
...Object.keys(fields),
|
|
1119
|
+
"id",
|
|
1120
|
+
"slug",
|
|
1121
|
+
"context"
|
|
1122
|
+
]);
|
|
1123
|
+
const diff = Object.keys(data).reduce(
|
|
1124
|
+
(acc, key) => {
|
|
1125
|
+
if (validKeys.has(key) && !this.areEquivalentValues(existing[key], data[key])) {
|
|
1126
|
+
acc[key] = data[key];
|
|
1127
|
+
}
|
|
1128
|
+
return acc;
|
|
1129
|
+
},
|
|
1130
|
+
{}
|
|
1131
|
+
);
|
|
1132
|
+
return Object.keys(diff).length > 0 ? diff : null;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Gets field definitions for the collection's item class
|
|
1136
|
+
*
|
|
1137
|
+
* @returns Object containing field definitions
|
|
1138
|
+
*/
|
|
1139
|
+
async getFields() {
|
|
1140
|
+
return await fieldsFromClass(this._itemClass);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Normalize user input into the model's logical field names before diffing or creation.
|
|
1144
|
+
*
|
|
1145
|
+
* Accepts both camelCase and snake_case keys while preserving framework meta fields.
|
|
1146
|
+
*/
|
|
1147
|
+
normalizeLogicalData(data) {
|
|
1148
|
+
const fields = this.getFieldsSync();
|
|
1149
|
+
const normalized = {};
|
|
1150
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1151
|
+
if (key.startsWith("_")) {
|
|
1152
|
+
normalized[key] = value;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const camelKey = key.includes("_") ? toCamelCase(key) : key;
|
|
1156
|
+
const snakeKey = key.includes("_") ? key : toSnakeCase(key);
|
|
1157
|
+
const outputKey = key in fields ? key : camelKey in fields ? camelKey : snakeKey in fields ? snakeKey : key;
|
|
1158
|
+
normalized[outputKey] = value;
|
|
1159
|
+
}
|
|
1160
|
+
return normalized;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Preserve persisted core timestamp fields during lightweight hydration.
|
|
1164
|
+
*/
|
|
1165
|
+
withHydratedCoreFields(data) {
|
|
1166
|
+
const hydratedData = { ...data };
|
|
1167
|
+
if (hydratedData.createdAt !== void 0 && hydratedData.created_at === void 0) {
|
|
1168
|
+
hydratedData.created_at = hydratedData.createdAt;
|
|
1169
|
+
}
|
|
1170
|
+
if (hydratedData.updatedAt !== void 0 && hydratedData.updated_at === void 0) {
|
|
1171
|
+
hydratedData.updated_at = hydratedData.updatedAt;
|
|
1172
|
+
}
|
|
1173
|
+
return hydratedData;
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Treat date-equivalent values as unchanged even if their runtime types differ.
|
|
1177
|
+
*/
|
|
1178
|
+
areEquivalentValues(existingValue, nextValue) {
|
|
1179
|
+
if (existingValue instanceof Date || nextValue instanceof Date) {
|
|
1180
|
+
const existingTime = this.toComparableTime(existingValue);
|
|
1181
|
+
const nextTime = this.toComparableTime(nextValue);
|
|
1182
|
+
if (existingTime !== null && nextTime !== null) {
|
|
1183
|
+
return existingTime === nextTime;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return existingValue === nextValue;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Convert supported date inputs into comparable timestamps.
|
|
1190
|
+
*/
|
|
1191
|
+
toComparableTime(value) {
|
|
1192
|
+
if (value instanceof Date) {
|
|
1193
|
+
const timestamp = value.getTime();
|
|
1194
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
1195
|
+
}
|
|
1196
|
+
if (typeof value === "string" && value.trim()) {
|
|
1197
|
+
const parsedDate = new Date(value);
|
|
1198
|
+
const timestamp = parsedDate.getTime();
|
|
1199
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
1200
|
+
}
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Gets field definitions synchronously from cache.
|
|
1205
|
+
*
|
|
1206
|
+
* This method provides sync access to fields for use in query methods,
|
|
1207
|
+
* avoiding the async overhead of getFields() on every query.
|
|
1208
|
+
* Fields are cached during create() initialization.
|
|
1209
|
+
*
|
|
1210
|
+
* @returns Map containing field definitions
|
|
1211
|
+
* @private
|
|
1212
|
+
*/
|
|
1213
|
+
getFieldsSync() {
|
|
1214
|
+
if (this._cachedFields) {
|
|
1215
|
+
return this._cachedFields;
|
|
1216
|
+
}
|
|
1217
|
+
const className = this.getResolvedItemClassName();
|
|
1218
|
+
const fields = ObjectRegistry.getFields(className);
|
|
1219
|
+
return Object.fromEntries(fields);
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Generates database schema for the collection's item class
|
|
1223
|
+
*
|
|
1224
|
+
* Leverages ObjectRegistry's cached schema for instant retrieval.
|
|
1225
|
+
*
|
|
1226
|
+
* @returns Schema object for database setup
|
|
1227
|
+
*/
|
|
1228
|
+
async generateSchema() {
|
|
1229
|
+
const { generateSchema } = await import("./schema/utils.js");
|
|
1230
|
+
return await generateSchema(this._itemClass);
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Gets the database table name for this collection
|
|
1234
|
+
*/
|
|
1235
|
+
get tableName() {
|
|
1236
|
+
if (!this._tableName) {
|
|
1237
|
+
const className = this.getResolvedItemClassName();
|
|
1238
|
+
const qualifiedName = this.getResolvedItemQualifiedName();
|
|
1239
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(qualifiedName);
|
|
1240
|
+
const fallbackTableName = this._itemClass.SMRT_TABLE_NAME || classnameToTablename(className);
|
|
1241
|
+
if (tableStrategy === "sti") {
|
|
1242
|
+
const stiBase = ObjectRegistry.getSTIBase(qualifiedName);
|
|
1243
|
+
if (stiBase) {
|
|
1244
|
+
const baseSchema = ObjectRegistry.getSchema(stiBase);
|
|
1245
|
+
if (baseSchema?.tableName) {
|
|
1246
|
+
this._tableName = baseSchema.tableName;
|
|
1247
|
+
} else {
|
|
1248
|
+
const ownSchema = ObjectRegistry.getSchema(className);
|
|
1249
|
+
this._tableName = ownSchema?.tableName || fallbackTableName;
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
const ownSchema = ObjectRegistry.getSchema(className);
|
|
1253
|
+
this._tableName = ownSchema?.tableName || fallbackTableName;
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
const ownSchema = ObjectRegistry.getSchema(className);
|
|
1257
|
+
this._tableName = ownSchema?.tableName || fallbackTableName;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return this._tableName;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Generates a table name from the collection class name
|
|
1264
|
+
*
|
|
1265
|
+
* @returns Generated table name
|
|
1266
|
+
*/
|
|
1267
|
+
generateTableName() {
|
|
1268
|
+
const tableName = this._className.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().replace(/([^s])$/, "$1s").replace(/y$/, "ies");
|
|
1269
|
+
return tableName;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Deletes a record from the collection by ID
|
|
1273
|
+
*
|
|
1274
|
+
* Loads the object and calls its delete() method, ensuring all interceptors
|
|
1275
|
+
* and lifecycle hooks (beforeDelete/afterDelete) are executed correctly.
|
|
1276
|
+
*
|
|
1277
|
+
* @param id - The ID of the record to delete
|
|
1278
|
+
* @returns Promise resolving to true if deleted, false if not found
|
|
1279
|
+
*
|
|
1280
|
+
* @example
|
|
1281
|
+
* ```typescript
|
|
1282
|
+
* const success = await collection.delete('some-uuid');
|
|
1283
|
+
* if (success) {
|
|
1284
|
+
* console.log('Record deleted');
|
|
1285
|
+
* }
|
|
1286
|
+
* ```
|
|
1287
|
+
*/
|
|
1288
|
+
async delete(id) {
|
|
1289
|
+
await ObjectRegistry.ensureManifestLoaded(this.getResolvedItemClassName());
|
|
1290
|
+
const instance = await this.get(id, { cache: false });
|
|
1291
|
+
if (!instance) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
await instance.delete();
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Returns the number of records matching the given filter conditions.
|
|
1299
|
+
*
|
|
1300
|
+
* Executes a `SELECT COUNT(*)` query with the same WHERE conversion as
|
|
1301
|
+
* `list()` (camelCase field names, operator suffixes, STI auto-filtering).
|
|
1302
|
+
* `limit`, `offset`, and `orderBy` are not applicable and are ignored.
|
|
1303
|
+
*
|
|
1304
|
+
* Note: `count()` is never served from the read cache (issue #1498) — only
|
|
1305
|
+
* `list()`/`get()` are. On a page that caches `list()`, a `count()` issued
|
|
1306
|
+
* in the same request reflects the live database and may briefly diverge
|
|
1307
|
+
* from the cached rows within the TTL window.
|
|
1308
|
+
*
|
|
1309
|
+
* @param options.where - Filter conditions (same syntax as `list()`)
|
|
1310
|
+
* @returns Total count of matching records as an integer
|
|
1311
|
+
*
|
|
1312
|
+
* @example
|
|
1313
|
+
* ```typescript
|
|
1314
|
+
* const total = await products.count();
|
|
1315
|
+
* const activeCount = await products.count({ where: { status: 'active' } });
|
|
1316
|
+
* const expensiveCount = await products.count({ where: { 'price >': 100 } });
|
|
1317
|
+
* ```
|
|
1318
|
+
*
|
|
1319
|
+
* @see {@link list} for retrieving the actual records
|
|
1320
|
+
*/
|
|
1321
|
+
async count(options = {}) {
|
|
1322
|
+
await this.ensureStorageReady();
|
|
1323
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
1324
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
1325
|
+
const interceptorContext = createInterceptorContext(
|
|
1326
|
+
itemClassName,
|
|
1327
|
+
"list",
|
|
1328
|
+
this.constructor.name
|
|
1329
|
+
);
|
|
1330
|
+
const interceptedOptions = await GlobalInterceptors.executeBeforeList(
|
|
1331
|
+
itemClassName,
|
|
1332
|
+
options,
|
|
1333
|
+
interceptorContext
|
|
1334
|
+
) ?? options ?? {};
|
|
1335
|
+
let { where } = interceptedOptions;
|
|
1336
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(itemQualifiedName);
|
|
1337
|
+
const isSTI = tableStrategy === "sti";
|
|
1338
|
+
if (isSTI) {
|
|
1339
|
+
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
1340
|
+
if (stiBase && stiBase !== itemQualifiedName) {
|
|
1341
|
+
where = {
|
|
1342
|
+
_meta_type: itemQualifiedName,
|
|
1343
|
+
...where || {}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
where = resolveMetaTypeInWhere(where);
|
|
1348
|
+
const { sql: whereSql, values: whereValues } = buildWhere(
|
|
1349
|
+
this.convertWhereKeys(where || {})
|
|
1350
|
+
);
|
|
1351
|
+
const result = await this.db.query(
|
|
1352
|
+
`SELECT COUNT(*) as count FROM ${this.tableName} ${whereSql}`,
|
|
1353
|
+
...whereValues
|
|
1354
|
+
);
|
|
1355
|
+
return Number.parseInt(result.rows[0].count, 10);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Execute a raw SQL query and hydrate results as collection item instances
|
|
1359
|
+
*
|
|
1360
|
+
* Provides full SQL power for complex queries (JOINs, CTEs, NOT EXISTS, etc.)
|
|
1361
|
+
* while still returning properly hydrated SMRT objects.
|
|
1362
|
+
*
|
|
1363
|
+
* @param sql - Raw SQL query string (should select from this.tableName)
|
|
1364
|
+
* @param params - Query parameters for prepared statement
|
|
1365
|
+
* @returns Promise resolving to array of hydrated model instances
|
|
1366
|
+
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* ```typescript
|
|
1369
|
+
* // Find meetings without corresponding recaps (NOT EXISTS pattern)
|
|
1370
|
+
* const meetings = await meetingCollection.query(`
|
|
1371
|
+
* SELECT m.* FROM meetings m
|
|
1372
|
+
* WHERE m.start_date < datetime('now')
|
|
1373
|
+
* AND NOT EXISTS (
|
|
1374
|
+
* SELECT 1 FROM contents c
|
|
1375
|
+
* WHERE c.meeting_id = m.id
|
|
1376
|
+
* AND c._meta_type = 'MeetingRecap'
|
|
1377
|
+
* )
|
|
1378
|
+
* ORDER BY m.start_date DESC
|
|
1379
|
+
* LIMIT ?
|
|
1380
|
+
* `, [10]);
|
|
1381
|
+
*
|
|
1382
|
+
* // Complex JOIN query
|
|
1383
|
+
* const products = await productCollection.query(`
|
|
1384
|
+
* SELECT p.* FROM products p
|
|
1385
|
+
* INNER JOIN categories c ON p.category_id = c.id
|
|
1386
|
+
* WHERE c.name = ? AND p.price > ?
|
|
1387
|
+
* ORDER BY p.price ASC
|
|
1388
|
+
* `, ['Electronics', 100]);
|
|
1389
|
+
* ```
|
|
1390
|
+
*/
|
|
1391
|
+
async query(sql, params = [], options = {}) {
|
|
1392
|
+
await this.ensureStorageReady();
|
|
1393
|
+
const interceptorContext = createInterceptorContext(
|
|
1394
|
+
this._itemClass.name,
|
|
1395
|
+
"query",
|
|
1396
|
+
this.constructor.name
|
|
1397
|
+
);
|
|
1398
|
+
const interceptedQuery = await GlobalInterceptors.executeBeforeQuery(
|
|
1399
|
+
this._itemClass.name,
|
|
1400
|
+
{ sql, params, allowRawOnTenantScoped: options.allowRawOnTenantScoped },
|
|
1401
|
+
interceptorContext
|
|
1402
|
+
);
|
|
1403
|
+
const result = await this.db.query(
|
|
1404
|
+
interceptedQuery.sql,
|
|
1405
|
+
...interceptedQuery.params
|
|
1406
|
+
);
|
|
1407
|
+
if (/\b(?:insert|update|delete|merge|truncate|replace)\b/i.test(
|
|
1408
|
+
interceptedQuery.sql
|
|
1409
|
+
)) {
|
|
1410
|
+
invalidateCollectionCache(resolveDbCacheKey(this.db), this.tableName);
|
|
1411
|
+
}
|
|
1412
|
+
const fields = this.getFieldsSync();
|
|
1413
|
+
const tableStrategy = ObjectRegistry.getTableStrategy(
|
|
1414
|
+
this.getResolvedItemQualifiedName()
|
|
1415
|
+
);
|
|
1416
|
+
const isSTI = tableStrategy === "sti";
|
|
1417
|
+
const instances = await Promise.all(
|
|
1418
|
+
result.rows.map((row) => this.hydrateResultRow(row, fields, isSTI))
|
|
1419
|
+
);
|
|
1420
|
+
return await GlobalInterceptors.executeAfterQuery(
|
|
1421
|
+
this._itemClass.name,
|
|
1422
|
+
instances,
|
|
1423
|
+
interceptorContext
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
async hydrateResultRow(row, fields, isSTI) {
|
|
1427
|
+
const formattedData = this.withHydratedCoreFields(
|
|
1428
|
+
formatDataJs(row, fields)
|
|
1429
|
+
);
|
|
1430
|
+
if (isSTI && formattedData._meta_type) {
|
|
1431
|
+
const polymorphicInstance = await this.createPolymorphic(
|
|
1432
|
+
formattedData._meta_type,
|
|
1433
|
+
formattedData,
|
|
1434
|
+
{ hydrateOnly: true }
|
|
1435
|
+
);
|
|
1436
|
+
polymorphicInstance.markAsPersisted();
|
|
1437
|
+
return polymorphicInstance;
|
|
1438
|
+
}
|
|
1439
|
+
const instanceParams = {
|
|
1440
|
+
ai: this.options.ai,
|
|
1441
|
+
db: this.db,
|
|
1442
|
+
_skipLoad: true,
|
|
1443
|
+
_reuseInitializedDb: true,
|
|
1444
|
+
_deferRuntimeInitialization: true,
|
|
1445
|
+
...formattedData
|
|
1446
|
+
};
|
|
1447
|
+
const instance = new this._itemClass(instanceParams);
|
|
1448
|
+
await instance.initialize();
|
|
1449
|
+
if (isSTI) {
|
|
1450
|
+
const registeredClass = ObjectRegistry.getClass(this._itemClass.name);
|
|
1451
|
+
instance._meta_type = registeredClass?.qualifiedName || this._itemClass.name;
|
|
1452
|
+
}
|
|
1453
|
+
instance.markAsPersisted();
|
|
1454
|
+
return instance;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Remember collection-level context
|
|
1458
|
+
*
|
|
1459
|
+
* Stores context applicable to all instances of this collection type.
|
|
1460
|
+
* Use for patterns that apply to the entire collection (e.g., default parsing strategies).
|
|
1461
|
+
*
|
|
1462
|
+
* @param options - Context options
|
|
1463
|
+
* @returns Promise that resolves when context is stored
|
|
1464
|
+
* @example
|
|
1465
|
+
* ```typescript
|
|
1466
|
+
* // Remember a default parsing strategy for all documents
|
|
1467
|
+
* await documentCollection.remember({
|
|
1468
|
+
* scope: 'parser/default',
|
|
1469
|
+
* key: 'selector',
|
|
1470
|
+
* value: { pattern: '.content article' },
|
|
1471
|
+
* confidence: 0.8
|
|
1472
|
+
* });
|
|
1473
|
+
*
|
|
1474
|
+
* // Update an existing context entry by specifying id
|
|
1475
|
+
* await documentCollection.remember({
|
|
1476
|
+
* id: 'existing-context-id',
|
|
1477
|
+
* scope: 'parser/default',
|
|
1478
|
+
* key: 'selector',
|
|
1479
|
+
* value: { pattern: '.content main article' },
|
|
1480
|
+
* confidence: 0.85
|
|
1481
|
+
* });
|
|
1482
|
+
* ```
|
|
1483
|
+
*/
|
|
1484
|
+
async remember(options) {
|
|
1485
|
+
if (!this.systemDb) {
|
|
1486
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
1487
|
+
}
|
|
1488
|
+
const id = options.id || crypto.randomUUID();
|
|
1489
|
+
const now = /* @__PURE__ */ new Date();
|
|
1490
|
+
await this.systemDb.upsert(
|
|
1491
|
+
"_smrt_contexts",
|
|
1492
|
+
// UNIQUE constraint: (owner_class, owner_id, scope, key, version)
|
|
1493
|
+
["owner_class", "owner_id", "scope", "key", "version"],
|
|
1494
|
+
{
|
|
1495
|
+
id,
|
|
1496
|
+
owner_class: this._itemClass.name,
|
|
1497
|
+
owner_id: "__collection__",
|
|
1498
|
+
scope: options.scope,
|
|
1499
|
+
key: options.key,
|
|
1500
|
+
value: JSON.stringify(options.value),
|
|
1501
|
+
metadata: options.metadata ? JSON.stringify(options.metadata) : null,
|
|
1502
|
+
version: options.version ?? 1,
|
|
1503
|
+
confidence: options.confidence ?? 1,
|
|
1504
|
+
success_count: 0,
|
|
1505
|
+
failure_count: 0,
|
|
1506
|
+
created_at: now,
|
|
1507
|
+
updated_at: now,
|
|
1508
|
+
last_used_at: now,
|
|
1509
|
+
expires_at: options.expiresAt ?? null
|
|
1510
|
+
}
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Recall collection-level context
|
|
1515
|
+
*
|
|
1516
|
+
* Retrieves context that applies to all instances of this collection.
|
|
1517
|
+
*
|
|
1518
|
+
* @param options - Recall options
|
|
1519
|
+
* @returns Promise resolving to the context value or null if not found
|
|
1520
|
+
* @example
|
|
1521
|
+
* ```typescript
|
|
1522
|
+
* // Recall default parsing strategy
|
|
1523
|
+
* const strategy = await documentCollection.recall({
|
|
1524
|
+
* scope: 'parser/default',
|
|
1525
|
+
* key: 'selector',
|
|
1526
|
+
* minConfidence: 0.5
|
|
1527
|
+
* });
|
|
1528
|
+
* ```
|
|
1529
|
+
*/
|
|
1530
|
+
async recall(options) {
|
|
1531
|
+
if (!this.systemDb) {
|
|
1532
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
1533
|
+
}
|
|
1534
|
+
let result;
|
|
1535
|
+
if (options.minConfidence !== void 0) {
|
|
1536
|
+
result = await this.systemDb.single`
|
|
1537
|
+
SELECT value, confidence
|
|
1538
|
+
FROM _smrt_contexts
|
|
1539
|
+
WHERE owner_class = ${this._itemClass.name}
|
|
1540
|
+
AND owner_id = ${"__collection__"}
|
|
1541
|
+
AND scope = ${options.scope}
|
|
1542
|
+
AND key = ${options.key}
|
|
1543
|
+
AND confidence >= ${options.minConfidence}
|
|
1544
|
+
ORDER BY confidence DESC, version DESC
|
|
1545
|
+
LIMIT 1
|
|
1546
|
+
`;
|
|
1547
|
+
} else {
|
|
1548
|
+
result = await this.systemDb.single`
|
|
1549
|
+
SELECT value, confidence
|
|
1550
|
+
FROM _smrt_contexts
|
|
1551
|
+
WHERE owner_class = ${this._itemClass.name}
|
|
1552
|
+
AND owner_id = ${"__collection__"}
|
|
1553
|
+
AND scope = ${options.scope}
|
|
1554
|
+
AND key = ${options.key}
|
|
1555
|
+
ORDER BY confidence DESC, version DESC
|
|
1556
|
+
LIMIT 1
|
|
1557
|
+
`;
|
|
1558
|
+
}
|
|
1559
|
+
if (result) {
|
|
1560
|
+
try {
|
|
1561
|
+
return JSON.parse(result.value);
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
logger.warn("Skipping corrupted _smrt_contexts value in recall()", {
|
|
1564
|
+
ownerClass: this._itemClass.name,
|
|
1565
|
+
scope: options.scope,
|
|
1566
|
+
key: options.key,
|
|
1567
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (options.includeAncestors) {
|
|
1572
|
+
const scopeParts = options.scope.split("/");
|
|
1573
|
+
while (scopeParts.length > 0) {
|
|
1574
|
+
scopeParts.pop();
|
|
1575
|
+
const parentScope = scopeParts.join("/") || "global";
|
|
1576
|
+
const parentResult = await this.recall({
|
|
1577
|
+
...options,
|
|
1578
|
+
scope: parentScope,
|
|
1579
|
+
includeAncestors: false
|
|
1580
|
+
});
|
|
1581
|
+
if (parentResult) return parentResult;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Recall all collection-level context in a scope
|
|
1588
|
+
*
|
|
1589
|
+
* Returns a Map of key -> value for all collection contexts matching the criteria.
|
|
1590
|
+
*
|
|
1591
|
+
* @param options - Recall options
|
|
1592
|
+
* @returns Promise resolving to Map of key -> value pairs
|
|
1593
|
+
* @example
|
|
1594
|
+
* ```typescript
|
|
1595
|
+
* // Get all default strategies
|
|
1596
|
+
* const strategies = await documentCollection.recallAll({
|
|
1597
|
+
* scope: 'parser/default',
|
|
1598
|
+
* minConfidence: 0.5
|
|
1599
|
+
* });
|
|
1600
|
+
* ```
|
|
1601
|
+
*/
|
|
1602
|
+
async recallAll(options = {}) {
|
|
1603
|
+
if (!this.systemDb) {
|
|
1604
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
1605
|
+
}
|
|
1606
|
+
const results = /* @__PURE__ */ new Map();
|
|
1607
|
+
let query = `
|
|
1608
|
+
SELECT key, value, confidence
|
|
1609
|
+
FROM _smrt_contexts
|
|
1610
|
+
WHERE owner_class = ? AND owner_id = ?
|
|
1611
|
+
`;
|
|
1612
|
+
const params = [this._itemClass.name, "__collection__"];
|
|
1613
|
+
if (options.scope) {
|
|
1614
|
+
if (options.includeDescendants) {
|
|
1615
|
+
query += ` AND (scope = ? OR scope LIKE ?)`;
|
|
1616
|
+
params.push(options.scope, `${options.scope}/%`);
|
|
1617
|
+
} else {
|
|
1618
|
+
query += ` AND scope = ?`;
|
|
1619
|
+
params.push(options.scope);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (options.minConfidence !== void 0) {
|
|
1623
|
+
query += ` AND confidence >= ?`;
|
|
1624
|
+
params.push(options.minConfidence);
|
|
1625
|
+
}
|
|
1626
|
+
query += ` ORDER BY confidence DESC`;
|
|
1627
|
+
const { rows } = await this.systemDb.query(query, ...params);
|
|
1628
|
+
for (const row of rows) {
|
|
1629
|
+
try {
|
|
1630
|
+
results.set(row.key, JSON.parse(row.value));
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
logger.warn("Skipping corrupted _smrt_contexts value in recallAll()", {
|
|
1633
|
+
ownerClass: this._itemClass.name,
|
|
1634
|
+
scope: options.scope,
|
|
1635
|
+
key: row.key,
|
|
1636
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return results;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Forget collection-level context
|
|
1644
|
+
*
|
|
1645
|
+
* Deletes collection context by scope and key.
|
|
1646
|
+
*
|
|
1647
|
+
* @param options - Context identification
|
|
1648
|
+
* @returns Promise that resolves when context is deleted
|
|
1649
|
+
* @example
|
|
1650
|
+
* ```typescript
|
|
1651
|
+
* // Remove a default strategy
|
|
1652
|
+
* await documentCollection.forget({
|
|
1653
|
+
* scope: 'parser/default',
|
|
1654
|
+
* key: 'selector'
|
|
1655
|
+
* });
|
|
1656
|
+
* ```
|
|
1657
|
+
*/
|
|
1658
|
+
async forget(options) {
|
|
1659
|
+
if (!this.systemDb) {
|
|
1660
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
1661
|
+
}
|
|
1662
|
+
await this.systemDb.query(
|
|
1663
|
+
`DELETE FROM _smrt_contexts
|
|
1664
|
+
WHERE owner_class = ? AND owner_id = ? AND scope = ? AND key = ?`,
|
|
1665
|
+
this._itemClass.name,
|
|
1666
|
+
"__collection__",
|
|
1667
|
+
options.scope,
|
|
1668
|
+
options.key
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Forget all collection-level context in a scope
|
|
1673
|
+
*
|
|
1674
|
+
* Deletes all collection contexts matching the scope pattern.
|
|
1675
|
+
*
|
|
1676
|
+
* @param options - Scope options
|
|
1677
|
+
* @returns Promise resolving to number of contexts deleted
|
|
1678
|
+
* @example
|
|
1679
|
+
* ```typescript
|
|
1680
|
+
* // Clear all default strategies
|
|
1681
|
+
* const count = await documentCollection.forgetScope({
|
|
1682
|
+
* scope: 'parser/default',
|
|
1683
|
+
* includeDescendants: true
|
|
1684
|
+
* });
|
|
1685
|
+
* ```
|
|
1686
|
+
*/
|
|
1687
|
+
async forgetScope(options) {
|
|
1688
|
+
if (!this.systemDb) {
|
|
1689
|
+
throw new Error("Database not initialized. Call initialize() first.");
|
|
1690
|
+
}
|
|
1691
|
+
let query = `
|
|
1692
|
+
DELETE FROM _smrt_contexts
|
|
1693
|
+
WHERE owner_class = ? AND owner_id = ?
|
|
1694
|
+
`;
|
|
1695
|
+
const params = [this._itemClass.name, "__collection__"];
|
|
1696
|
+
if (options.includeDescendants) {
|
|
1697
|
+
query += ` AND (scope = ? OR scope LIKE ?)`;
|
|
1698
|
+
params.push(options.scope, `${options.scope}/%`);
|
|
1699
|
+
} else {
|
|
1700
|
+
query += ` AND scope = ?`;
|
|
1701
|
+
params.push(options.scope);
|
|
1702
|
+
}
|
|
1703
|
+
const { rowCount } = await this.systemDb.query(query, ...params);
|
|
1704
|
+
return rowCount || 0;
|
|
1705
|
+
}
|
|
1706
|
+
// ============================================================================
|
|
1707
|
+
// Semantic Search Methods
|
|
1708
|
+
// ============================================================================
|
|
1709
|
+
/**
|
|
1710
|
+
* Semantic search by text query
|
|
1711
|
+
*
|
|
1712
|
+
* Generates an embedding for the query text and finds similar objects
|
|
1713
|
+
* based on cosine similarity of stored embeddings.
|
|
1714
|
+
*
|
|
1715
|
+
* @param query - Text to search for
|
|
1716
|
+
* @param options - Search options
|
|
1717
|
+
* @param options.field - Specific field to search (defaults to first embedding field)
|
|
1718
|
+
* @param options.limit - Maximum results to return (default: 10)
|
|
1719
|
+
* @param options.minSimilarity - Minimum similarity threshold 0-1 (default: 0)
|
|
1720
|
+
* @param options.where - Additional WHERE filters to apply
|
|
1721
|
+
* @returns Promise resolving to array of objects with _similarity score
|
|
1722
|
+
*
|
|
1723
|
+
* @example
|
|
1724
|
+
* ```typescript
|
|
1725
|
+
* const results = await articles.semanticSearch('machine learning trends', {
|
|
1726
|
+
* limit: 10,
|
|
1727
|
+
* minSimilarity: 0.7
|
|
1728
|
+
* });
|
|
1729
|
+
*
|
|
1730
|
+
* for (const article of results) {
|
|
1731
|
+
* console.log(`${article.title} (similarity: ${article._similarity})`);
|
|
1732
|
+
* }
|
|
1733
|
+
* ```
|
|
1734
|
+
*/
|
|
1735
|
+
async semanticSearch(query, options = {}) {
|
|
1736
|
+
const { field, limit = 10, minSimilarity = 0, where } = options;
|
|
1737
|
+
const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(
|
|
1738
|
+
this._itemClass.name
|
|
1739
|
+
);
|
|
1740
|
+
if (!embeddingConfig) {
|
|
1741
|
+
throw new Error(
|
|
1742
|
+
`No embedding configuration found for ${this._itemClass.name}. Add embeddings config to @smrt() decorator.`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const searchField = field || embeddingConfig.fields[0];
|
|
1746
|
+
if (!embeddingConfig.fields.includes(searchField)) {
|
|
1747
|
+
throw new Error(
|
|
1748
|
+
`Field '${searchField}' is not configured for embeddings on ${this._itemClass.name}. Available fields: ${embeddingConfig.fields.join(", ")}`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
const provider = new EmbeddingProvider(
|
|
1752
|
+
{
|
|
1753
|
+
dimensions: embeddingConfig.dimensions,
|
|
1754
|
+
provider: embeddingConfig.provider,
|
|
1755
|
+
localModel: embeddingConfig.localModel,
|
|
1756
|
+
aiModel: embeddingConfig.aiModel,
|
|
1757
|
+
fallbackToAI: embeddingConfig.fallbackToAI
|
|
1758
|
+
},
|
|
1759
|
+
this.ai
|
|
1760
|
+
);
|
|
1761
|
+
const [queryEmbedding] = await provider.embed(query);
|
|
1762
|
+
return this.findSimilarToEmbedding(queryEmbedding, {
|
|
1763
|
+
field: searchField,
|
|
1764
|
+
limit,
|
|
1765
|
+
minSimilarity,
|
|
1766
|
+
where
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Find objects similar to a given object
|
|
1771
|
+
*
|
|
1772
|
+
* Uses stored embeddings to find objects most similar to the provided object.
|
|
1773
|
+
*
|
|
1774
|
+
* @param object - Object to find similar items for (or object ID)
|
|
1775
|
+
* @param options - Search options
|
|
1776
|
+
* @param options.field - Specific field to compare (defaults to first embedding field)
|
|
1777
|
+
* @param options.limit - Maximum results to return (default: 5)
|
|
1778
|
+
* @param options.excludeSelf - Whether to exclude the source object (default: true)
|
|
1779
|
+
* @returns Promise resolving to array of similar objects with _similarity score
|
|
1780
|
+
*
|
|
1781
|
+
* @example
|
|
1782
|
+
* ```typescript
|
|
1783
|
+
* const article = await articles.get('some-article-id');
|
|
1784
|
+
* const similar = await articles.findSimilar(article, {
|
|
1785
|
+
* limit: 5,
|
|
1786
|
+
* excludeSelf: true
|
|
1787
|
+
* });
|
|
1788
|
+
* ```
|
|
1789
|
+
*/
|
|
1790
|
+
async findSimilar(object, options = {}) {
|
|
1791
|
+
const { field, limit = 5, excludeSelf = true } = options;
|
|
1792
|
+
let sourceObject;
|
|
1793
|
+
if (typeof object === "string") {
|
|
1794
|
+
const found = await this.get(object);
|
|
1795
|
+
if (!found) {
|
|
1796
|
+
throw new Error(`Object not found: ${object}`);
|
|
1797
|
+
}
|
|
1798
|
+
sourceObject = found;
|
|
1799
|
+
} else {
|
|
1800
|
+
sourceObject = object;
|
|
1801
|
+
}
|
|
1802
|
+
const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(
|
|
1803
|
+
this._itemClass.name
|
|
1804
|
+
);
|
|
1805
|
+
if (!embeddingConfig) {
|
|
1806
|
+
throw new Error(
|
|
1807
|
+
`No embedding configuration found for ${this._itemClass.name}.`
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
const searchField = field || embeddingConfig.fields[0];
|
|
1811
|
+
const provider = new EmbeddingProvider(
|
|
1812
|
+
{
|
|
1813
|
+
dimensions: embeddingConfig.dimensions,
|
|
1814
|
+
provider: embeddingConfig.provider,
|
|
1815
|
+
localModel: embeddingConfig.localModel,
|
|
1816
|
+
aiModel: embeddingConfig.aiModel,
|
|
1817
|
+
fallbackToAI: embeddingConfig.fallbackToAI
|
|
1818
|
+
},
|
|
1819
|
+
this.ai
|
|
1820
|
+
);
|
|
1821
|
+
const model = provider.getModelName();
|
|
1822
|
+
const storedEmbedding = await EmbeddingStorage.get(
|
|
1823
|
+
this.systemDb,
|
|
1824
|
+
this._itemClass.name,
|
|
1825
|
+
sourceObject.id,
|
|
1826
|
+
searchField,
|
|
1827
|
+
model
|
|
1828
|
+
);
|
|
1829
|
+
if (!storedEmbedding) {
|
|
1830
|
+
throw new Error(
|
|
1831
|
+
`No embedding found for object ${sourceObject.id} field '${searchField}'. Generate embeddings first with object.generateEmbeddings().`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
const where = excludeSelf ? { "id !=": sourceObject.id } : void 0;
|
|
1835
|
+
return this.findSimilarToEmbedding(storedEmbedding.embedding, {
|
|
1836
|
+
field: searchField,
|
|
1837
|
+
limit,
|
|
1838
|
+
minSimilarity: 0,
|
|
1839
|
+
where
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Find objects similar to a raw embedding vector
|
|
1844
|
+
*
|
|
1845
|
+
* Low-level method for finding objects by embedding similarity.
|
|
1846
|
+
*
|
|
1847
|
+
* @param embedding - Embedding vector to compare against
|
|
1848
|
+
* @param options - Search options
|
|
1849
|
+
* @param options.field - Field name to search (defaults to first embedding field)
|
|
1850
|
+
* @param options.limit - Maximum results to return (default: 10)
|
|
1851
|
+
* @param options.minSimilarity - Minimum similarity threshold 0-1 (default: 0)
|
|
1852
|
+
* @param options.where - Additional WHERE filters to apply
|
|
1853
|
+
* @returns Promise resolving to array of objects with _similarity score
|
|
1854
|
+
*
|
|
1855
|
+
* @example
|
|
1856
|
+
* ```typescript
|
|
1857
|
+
* // Using a pre-computed embedding
|
|
1858
|
+
* const results = await articles.findSimilarToEmbedding(myEmbedding, {
|
|
1859
|
+
* limit: 10,
|
|
1860
|
+
* minSimilarity: 0.5
|
|
1861
|
+
* });
|
|
1862
|
+
* ```
|
|
1863
|
+
*/
|
|
1864
|
+
async findSimilarToEmbedding(embedding, options = {}) {
|
|
1865
|
+
const { field, limit = 10, minSimilarity = 0, where } = options;
|
|
1866
|
+
const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(
|
|
1867
|
+
this._itemClass.name
|
|
1868
|
+
);
|
|
1869
|
+
if (!embeddingConfig) {
|
|
1870
|
+
throw new Error(
|
|
1871
|
+
`No embedding configuration found for ${this._itemClass.name}.`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
const searchField = field || embeddingConfig.fields[0];
|
|
1875
|
+
const provider = new EmbeddingProvider(
|
|
1876
|
+
{
|
|
1877
|
+
dimensions: embeddingConfig.dimensions,
|
|
1878
|
+
provider: embeddingConfig.provider,
|
|
1879
|
+
localModel: embeddingConfig.localModel,
|
|
1880
|
+
aiModel: embeddingConfig.aiModel,
|
|
1881
|
+
fallbackToAI: embeddingConfig.fallbackToAI
|
|
1882
|
+
},
|
|
1883
|
+
this.ai
|
|
1884
|
+
);
|
|
1885
|
+
const model = provider.getModelName();
|
|
1886
|
+
const projectConfig = ObjectRegistry.getProjectEmbeddingConfig();
|
|
1887
|
+
const storage = projectConfig?.storage || "json";
|
|
1888
|
+
const vector = storage === "native" ? this.systemDb.vector : void 0;
|
|
1889
|
+
const scored = await EmbeddingStorage.searchSimilar(
|
|
1890
|
+
this.systemDb,
|
|
1891
|
+
this._itemClass.name,
|
|
1892
|
+
embedding,
|
|
1893
|
+
{
|
|
1894
|
+
field: searchField,
|
|
1895
|
+
model,
|
|
1896
|
+
limit,
|
|
1897
|
+
minSimilarity
|
|
1898
|
+
},
|
|
1899
|
+
vector
|
|
1900
|
+
);
|
|
1901
|
+
if (scored.length === 0) {
|
|
1902
|
+
return [];
|
|
1903
|
+
}
|
|
1904
|
+
const objectIds = scored.map((s) => s.objectId);
|
|
1905
|
+
const similarityMap = new Map(
|
|
1906
|
+
scored.map((s) => [s.objectId, s.similarity])
|
|
1907
|
+
);
|
|
1908
|
+
const whereClause = where ? { "id in": objectIds, ...where } : { "id in": objectIds };
|
|
1909
|
+
const objects = await this.list({
|
|
1910
|
+
where: whereClause
|
|
1911
|
+
});
|
|
1912
|
+
const results = objects.map((obj) => {
|
|
1913
|
+
obj._similarity = similarityMap.get(obj.id) || 0;
|
|
1914
|
+
return obj;
|
|
1915
|
+
});
|
|
1916
|
+
results.sort((a, b) => b._similarity - a._similarity);
|
|
1917
|
+
return results;
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Generate missing embeddings for all objects in the collection
|
|
1921
|
+
*
|
|
1922
|
+
* Batch generates embeddings for objects that don't have them yet
|
|
1923
|
+
* or have stale embeddings.
|
|
1924
|
+
*
|
|
1925
|
+
* @param options - Generation options
|
|
1926
|
+
* @param options.batchSize - Number of objects to process at once (default: 50)
|
|
1927
|
+
* @param options.onProgress - Progress callback
|
|
1928
|
+
* @returns Promise resolving to generation statistics
|
|
1929
|
+
*
|
|
1930
|
+
* @example
|
|
1931
|
+
* ```typescript
|
|
1932
|
+
* const stats = await articles.generateMissingEmbeddings({
|
|
1933
|
+
* batchSize: 100,
|
|
1934
|
+
* onProgress: ({ completed, total }) => {
|
|
1935
|
+
* console.log(`Progress: ${completed}/${total}`);
|
|
1936
|
+
* }
|
|
1937
|
+
* });
|
|
1938
|
+
*
|
|
1939
|
+
* console.log(`Generated: ${stats.generated}, Skipped: ${stats.skipped}`);
|
|
1940
|
+
* ```
|
|
1941
|
+
*/
|
|
1942
|
+
async generateMissingEmbeddings(options = {}) {
|
|
1943
|
+
const { batchSize = 50, onProgress } = options;
|
|
1944
|
+
const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(
|
|
1945
|
+
this._itemClass.name
|
|
1946
|
+
);
|
|
1947
|
+
if (!embeddingConfig) {
|
|
1948
|
+
throw new Error(
|
|
1949
|
+
`No embedding configuration found for ${this._itemClass.name}.`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
const total = await this.count({});
|
|
1953
|
+
let completed = 0;
|
|
1954
|
+
let generated = 0;
|
|
1955
|
+
let skipped = 0;
|
|
1956
|
+
let offset = 0;
|
|
1957
|
+
while (offset < total) {
|
|
1958
|
+
const batch = await this.list({ limit: batchSize, offset });
|
|
1959
|
+
for (const obj of batch) {
|
|
1960
|
+
try {
|
|
1961
|
+
const hasStale = await obj.hasStaleEmbeddings();
|
|
1962
|
+
if (hasStale) {
|
|
1963
|
+
await obj.generateEmbeddings();
|
|
1964
|
+
generated++;
|
|
1965
|
+
} else {
|
|
1966
|
+
skipped++;
|
|
1967
|
+
}
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
logger.warn(`Failed to generate embeddings for ${obj.id}`, {
|
|
1970
|
+
error: error instanceof Error ? error.message : error
|
|
1971
|
+
});
|
|
1972
|
+
skipped++;
|
|
1973
|
+
}
|
|
1974
|
+
completed++;
|
|
1975
|
+
if (onProgress) {
|
|
1976
|
+
onProgress({ completed, total });
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
offset += batchSize;
|
|
1980
|
+
}
|
|
1981
|
+
return { generated, skipped };
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
export {
|
|
1985
|
+
SmrtCollection
|
|
1986
|
+
};
|
|
1987
|
+
//# sourceMappingURL=collection.js.map
|