@happyvertical/smrt-core 0.40.69 → 0.41.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 +29 -4
- package/README.md +20 -1
- package/agents/change-feed.md +1 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +786 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +5 -3
- package/dist/cascade.d.ts +120 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +430 -0
- package/dist/cascade.js.map +1 -0
- package/dist/change-feed.d.ts +34 -2
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +52 -11
- package/dist/change-feed.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +87 -9
- package/dist/class.js.map +1 -1
- package/dist/collection-cache.js +0 -0
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +130 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +290 -57
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +80 -6
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +102 -12
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +19 -4
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/types.d.ts +5 -0
- package/dist/dispatch/types.d.ts.map +1 -1
- package/dist/embedded-write-queue.d.ts +46 -0
- package/dist/embedded-write-queue.d.ts.map +1 -0
- package/dist/embedded-write-queue.js +66 -0
- package/dist/embedded-write-queue.js.map +1 -0
- package/dist/embeddings/storage.d.ts +7 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +29 -12
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +31 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +27 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +4 -7
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/static-manifest.js +10 -10
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +19 -19
- package/dist/migrations/differ.d.ts +211 -9
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +613 -50
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +4 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +6 -3
- package/dist/migrations/minor-units.d.ts +162 -0
- package/dist/migrations/minor-units.d.ts.map +1 -0
- package/dist/migrations/minor-units.js +381 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +6 -3
- package/dist/object.d.ts +142 -10
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +196 -41
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +101 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +177 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/manifest-field-merge.d.ts +12 -0
- package/dist/registry/manifest-field-merge.d.ts.map +1 -1
- package/dist/registry/manifest-field-merge.js +14 -2
- package/dist/registry/manifest-field-merge.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +22 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +205 -165
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +35 -3
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +41 -46
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +61 -83
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +45 -0
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +92 -28
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +104 -0
- package/dist/schema/conflict-target.d.ts.map +1 -0
- package/dist/schema/conflict-target.js +129 -0
- package/dist/schema/conflict-target.js.map +1 -0
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +2 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +12 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +307 -6
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +510 -87
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index-utils.d.ts +120 -0
- package/dist/schema/index-utils.d.ts.map +1 -1
- package/dist/schema/index-utils.js +242 -1
- package/dist/schema/index-utils.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +602 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +65 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +187 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +2 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +5 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +20 -8
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/system/compatibility.d.ts +42 -0
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +182 -9
- package/dist/system/compatibility.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +3 -2
- package/dist/system/retention.d.ts +237 -0
- package/dist/system/retention.d.ts.map +1 -0
- package/dist/system/retention.js +497 -0
- package/dist/system/retention.js.map +1 -0
- package/dist/system/schema.d.ts +100 -15
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +81 -45
- package/dist/system/schema.js.map +1 -1
- package/dist/system/types.d.ts +0 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -9
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-cache.js","names":[],"sources":["../src/collection-cache.ts"],"sourcesContent":["/**\n * Opt-in read-through cache for collection reads (issue #1498).\n *\n * SSR-rendered SMRT apps re-query read-heavy / write-rare collections on\n * every request, and the per-query round-trip dominates wall time when a\n * page needs many collections. This module memoizes `list()`/`get()` row\n * sets keyed by the final SQL + parameters, for an opt-in TTL.\n *\n * Correctness model:\n * - Caching is OFF by default. It is enabled per call\n * (`list({ cache: { ttl } })`) or per model (`@smrt({ cache: { ttl } })`).\n * - SMRT owns every mutation path (`save()`/`delete()` back\n * `collection.create()`, `getOrUpsert()`, junction attach/detach), so all\n * writes invalidate the affected table's entries in-process automatically.\n * - Entries are scoped per database identity (`db.url`) and per table, so\n * multi-DB processes and STI siblings (which share a table) stay coherent.\n * - Caches are per-process. With multiple replicas, a local invalidation\n * leaves peers stale until TTL unless cross-process invalidation is opted\n * into (`crossProcess: true`), which broadcasts over the database\n * adapter's notification capability (e.g. Postgres LISTEN/NOTIFY) when\n * the adapter provides one.\n *\n * Cached values are raw result rows, not hydrated instances — hydration and\n * read interceptors (tenancy, audit) run on every call, cached or not, and\n * each caller receives isolated row copies.\n *\n * Known limitations:\n * - Invalidation fires when a mutation's SQL executes, not when its\n * surrounding transaction commits. A write inside an uncommitted\n * transaction invalidates (and may broadcast) immediately; a concurrent\n * reader could repopulate the cache from the pre-commit snapshot, and a\n * rollback leaves the cache invalidated for a write that never landed.\n * Caching targets read-heavy / write-rare data where this is rare; for\n * models mutated inside multi-statement transactions, coherence is still\n * bounded by TTL. Per-call `cache: false` forces a fresh read where it\n * matters.\n * - Writes that bypass the framework's mutation paths (raw `db.query`\n * issued outside `collection.query()`, external processes without\n * `crossProcess`) are only bounded by TTL.\n *\n * @see https://github.com/happyvertical/smrt/issues/1498\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Read-through cache configuration for collection reads.\n *\n * Used both per call (`collection.list({ cache: { ttl: 60_000 } })`) and per\n * model (`@smrt({ cache: { ttl: 60_000 } })`).\n */\nexport interface CollectionCacheConfig {\n /**\n * Time-to-live for cached query results, in milliseconds. Must be > 0.\n */\n ttl: number;\n\n /**\n * Broadcast invalidations to peer processes through the database\n * adapter's notification capability (`db.notifications`, e.g. Postgres\n * LISTEN/NOTIFY). Without this, peer replicas serve stale rows until TTL.\n *\n * Model-level config (`@smrt({ cache })`) is the reliable opt-in: every\n * process that writes the model knows to broadcast. As a per-call option,\n * writes broadcast only from processes that have already performed a\n * `crossProcess` cached read of the same table (typical for homogeneous\n * replicas running the same routes); a process that only writes never\n * learns about the per-call opt-in, so its peers fall back to TTL expiry.\n *\n * Ignored (with a one-time warning) when the adapter does not expose\n * notifications.\n */\n crossProcess?: boolean;\n}\n\ninterface CacheEntry {\n expiresAt: number;\n rows: Record<string, unknown>[];\n}\n\n/**\n * Per-table entry cap. A table accumulating more distinct query shapes than\n * this within one TTL window evicts its oldest entries (insertion order).\n * Guards against unbounded growth from high-cardinality WHERE values.\n */\nconst MAX_ENTRIES_PER_TABLE = 500;\n\n/**\n * Notification channel used for cross-process invalidation broadcasts.\n */\nexport const CACHE_INVALIDATION_CHANNEL = 'smrt_collection_cache';\n\n/**\n * Identifies this process in broadcast payloads so a replica can skip\n * notifications it published itself (it already invalidated locally).\n *\n * Exported so sibling cross-replica buses (the #1763 change-signal bus) share\n * one per-process identity, keeping echo-avoidance consistent across channels.\n */\nexport const PROCESS_ID = crypto.randomUUID();\n\n/**\n * dbKey → tableName → (queryKey → entry)\n */\nconst store = new Map<string, Map<string, Map<string, CacheEntry>>>();\n\n/**\n * Monotonic invalidation generation per `dbKey\\0tableName`, bumped on every\n * invalidation. A read captures the generation *before* its DB round-trip and\n * passes it to `setCachedRows`; if an invalidating write landed during the\n * round-trip, the generation no longer matches and the (now-stale) result is\n * dropped instead of cached. This closes the read-miss/concurrent-write race\n * where an in-flight SELECT would otherwise repopulate the cache with\n * pre-write rows for the full TTL.\n */\nconst generations = new Map<string, number>();\n\nfunction generationKey(dbKey: string, tableName: string): string {\n return `${dbKey}\u0000${tableName}`;\n}\n\n/**\n * Current invalidation generation for a table (0 if never invalidated).\n * Capture this before a DB read and pass it to `setCachedRows`.\n */\nexport function getCacheGeneration(dbKey: string, tableName: string): number {\n return generations.get(generationKey(dbKey, tableName)) ?? 0;\n}\n\n/**\n * Fallback identities for database instances that expose no URL\n * (each such instance gets its own scope, never shared).\n */\nconst fallbackDbKeys = new WeakMap<object, string>();\n\n/**\n * Resolve a stable cache scope for a database instance.\n *\n * Mirrors table-verifier's identity resolution (`db.url || config.url`).\n * `:memory:` databases share that URL string while being entirely separate\n * databases, so they (and URL-less instances) are scoped per instance —\n * serving one in-memory database's rows for another would be a correctness\n * bug, not just a stale read.\n */\nexport function resolveDbCacheKey(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = db.url || dbWithConfig.config?.url;\n if (url && url !== ':memory:') return url;\n\n let key = fallbackDbKeys.get(db);\n if (!key) {\n key = `smrt-db:${crypto.randomUUID()}`;\n fallbackDbKeys.set(db, key);\n }\n return key;\n}\n\n/**\n * Build the cache key for a query. The final SQL and bound parameters fully\n * normalize the query shape — they already include STI discriminator\n * filters, interceptor-injected tenant filters, ORDER BY, LIMIT and OFFSET.\n */\nexport function buildQueryCacheKey(sql: string, params: unknown[]): string {\n return `${sql}\u0000${JSON.stringify(params)}`;\n}\n\n/**\n * Read cached rows for a query, or undefined on miss/expiry.\n *\n * Returns a structured clone so callers can never mutate the cached copy\n * (hydration writes into row objects).\n */\nexport function getCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n): Record<string, unknown>[] | undefined {\n const entry = store.get(dbKey)?.get(tableName)?.get(queryKey);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n store.get(dbKey)?.get(tableName)?.delete(queryKey);\n return undefined;\n }\n return structuredClone(entry.rows);\n}\n\n/**\n * Store rows for a query under the table's cache scope.\n *\n * `expectedGeneration` is the value {@link getCacheGeneration} returned before\n * the DB read. If an invalidation bumped the table's generation while the read\n * was in flight, the result is stale and is dropped rather than cached.\n */\nexport function setCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n rows: Record<string, unknown>[],\n ttl: number,\n expectedGeneration?: number,\n): void {\n if (!(ttl > 0)) return;\n\n // Drop a result that a concurrent write invalidated mid-flight.\n if (\n expectedGeneration !== undefined &&\n getCacheGeneration(dbKey, tableName) !== expectedGeneration\n ) {\n return;\n }\n\n let tables = store.get(dbKey);\n if (!tables) {\n tables = new Map();\n store.set(dbKey, tables);\n }\n let entries = tables.get(tableName);\n if (!entries) {\n entries = new Map();\n tables.set(tableName, entries);\n }\n\n // Evict oldest entries (Map preserves insertion order) at the cap. Skip when\n // refreshing an existing key — `set` updates it in place without growing the\n // map, so evicting first would drop a live entry and shrink the effective cap.\n if (!entries.has(queryKey)) {\n while (entries.size >= MAX_ENTRIES_PER_TABLE) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n }\n\n entries.set(queryKey, {\n expiresAt: Date.now() + ttl,\n rows: structuredClone(rows),\n });\n}\n\n/**\n * Drop every cached entry for a table. Called by the framework after any\n * successful mutation against that table — this is the write-invalidation\n * guarantee an app-side cache can't make.\n */\nexport function invalidateCollectionCache(\n dbKey: string,\n tableName: string,\n): void {\n store.get(dbKey)?.delete(tableName);\n // Bump the generation so any read whose DB round-trip is still in flight\n // discards its (now-stale) result instead of repopulating the table.\n const gkey = generationKey(dbKey, tableName);\n generations.set(gkey, (generations.get(gkey) ?? 0) + 1);\n}\n\n/**\n * Clear all cached collection reads and stop cross-process listeners.\n * Call in test setup to ensure isolation between test files.\n */\nexport function resetCollectionCache(): void {\n store.clear();\n generations.clear();\n crossProcessInterest.clear();\n stopCacheInvalidationListeners();\n}\n\n// ============================================================================\n// Per-call crossProcess interest (write-side broadcast decision)\n// ============================================================================\n\n/**\n * dbKey → tables this process has cached with a per-call `crossProcess`\n * opt-in. Writes consult this so per-call usage broadcasts too — model-level\n * config can't be the only trigger when the opt-in lives at the call site.\n */\nconst crossProcessInterest = new Map<string, Set<string>>();\n\n/**\n * Record that a per-call `crossProcess` cached read happened for a table.\n */\nexport function registerCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): void {\n let tables = crossProcessInterest.get(dbKey);\n if (!tables) {\n tables = new Set();\n crossProcessInterest.set(dbKey, tables);\n }\n tables.add(tableName);\n}\n\n/**\n * Whether any per-call `crossProcess` cached read has touched this table.\n */\nexport function hasCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): boolean {\n return crossProcessInterest.get(dbKey)?.has(tableName) ?? false;\n}\n\n// ============================================================================\n// Cross-process invalidation (opt-in, via db.notifications)\n// ============================================================================\n\ninterface ListenerHandle {\n iterator: AsyncIterator<unknown> | null;\n stopped: boolean;\n}\n\nconst listeners = new Map<string, ListenerHandle>();\nconst warnedNoNotifications = new Set<string>();\n\n/**\n * The database adapter's optional notification capability (e.g. Postgres\n * LISTEN/NOTIFY), or `undefined` when the adapter exposes none. Duck-typed so\n * core never depends on a concrete adapter shape. Exported so sibling\n * cross-replica buses (the #1763 change-signal bus) resolve it identically.\n */\nexport function getNotifications(db: DatabaseInterface) {\n return (db as DatabaseInterface & { notifications?: unknown })\n .notifications as\n | {\n notify(channel: string, payload: unknown): Promise<number>;\n listen(\n channel: string,\n options?: Record<string, unknown>,\n ): AsyncIterable<{ channel: string; payload: unknown }>;\n }\n | undefined;\n}\n\nfunction warnOnceNoNotifications(dbKey: string, context: string): void {\n if (warnedNoNotifications.has(dbKey)) return;\n warnedNoNotifications.add(dbKey);\n logger.warn(\n `Collection cache: crossProcess invalidation requested but the database ` +\n `adapter exposes no notification capability (${context}). Peer ` +\n `replicas will serve stale rows until TTL.`,\n );\n}\n\n/**\n * Broadcast a table invalidation to peer processes.\n *\n * Fire-and-forget from mutation paths: a broadcast failure must never fail\n * the write that triggered it.\n */\nexport async function broadcastCacheInvalidation(\n db: DatabaseInterface,\n tableName: string,\n): Promise<void> {\n const notifications = getNotifications(db);\n const dbKey = resolveDbCacheKey(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'broadcast');\n return;\n }\n try {\n await notifications.notify(CACHE_INVALIDATION_CHANNEL, {\n table: tableName,\n source: PROCESS_ID,\n });\n } catch (error) {\n logger.warn(\n `Collection cache: failed to broadcast invalidation for ${tableName}`,\n { error: error instanceof Error ? error.message : error },\n );\n }\n}\n\n/**\n * Ensure a background listener consumes invalidation broadcasts for this\n * database and drops matching local cache entries.\n *\n * Started lazily by the first cached read that opted into `crossProcess`.\n * Notifications published by this process are skipped — the local\n * invalidation already happened synchronously on the write path.\n */\nexport function ensureCacheInvalidationListener(db: DatabaseInterface): void {\n const dbKey = resolveDbCacheKey(db);\n if (listeners.has(dbKey)) return;\n\n const notifications = getNotifications(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'listen');\n return;\n }\n\n const handle: ListenerHandle = { iterator: null, stopped: false };\n listeners.set(dbKey, handle);\n\n void (async () => {\n try {\n const iterable = notifications.listen(CACHE_INVALIDATION_CHANNEL);\n const iterator = iterable[Symbol.asyncIterator]();\n handle.iterator = iterator;\n\n while (!handle.stopped) {\n const { value, done } = await iterator.next();\n if (done || handle.stopped) break;\n\n const notification = value as { payload?: unknown };\n const payload: unknown =\n typeof notification?.payload === 'string'\n ? safeParse(notification.payload)\n : notification?.payload;\n\n if (!payload || typeof payload !== 'object') continue;\n const record = payload as { table?: unknown; source?: unknown };\n if (typeof record.table !== 'string') continue;\n if (record.source === PROCESS_ID) continue;\n\n invalidateCollectionCache(dbKey, record.table);\n }\n } catch (error) {\n if (!handle.stopped) {\n logger.warn(\n 'Collection cache: invalidation listener terminated unexpectedly; ' +\n 'cross-process invalidation is inactive for this database ' +\n '(local TTL still bounds staleness)',\n { error: error instanceof Error ? error.message : error },\n );\n }\n } finally {\n // Only retract our own handle. A concurrent stop+restart for the same\n // dbKey may have already installed a replacement; deleting unconditionally\n // would orphan it and leak/duplicate the live listener.\n if (listeners.get(dbKey) === handle) {\n listeners.delete(dbKey);\n }\n }\n })();\n}\n\n/**\n * Stop all cross-process invalidation listeners. Used by tests and during\n * shutdown; safe to call when none are active.\n */\nexport function stopCacheInvalidationListeners(): void {\n for (const handle of listeners.values()) {\n handle.stopped = true;\n void handle.iterator?.return?.(undefined);\n }\n listeners.clear();\n warnedNoNotifications.clear();\n}\n\nfunction safeParse(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection-cache.js","names":[],"sources":["../src/collection-cache.ts"],"sourcesContent":["/**\n * Opt-in read-through cache for collection reads (issue #1498).\n *\n * SSR-rendered SMRT apps re-query read-heavy / write-rare collections on\n * every request, and the per-query round-trip dominates wall time when a\n * page needs many collections. This module memoizes `list()`/`get()` row\n * sets keyed by the final SQL + parameters, for an opt-in TTL.\n *\n * Correctness model:\n * - Caching is OFF by default. It is enabled per call\n * (`list({ cache: { ttl } })`) or per model (`@smrt({ cache: { ttl } })`).\n * - SMRT owns every mutation path (`save()`/`delete()` back\n * `collection.create()`, `getOrUpsert()`, junction attach/detach), so all\n * writes invalidate the affected table's entries in-process automatically.\n * - Entries are scoped per database identity (`db.url`) and per table, so\n * multi-DB processes and STI siblings (which share a table) stay coherent.\n * - Caches are per-process. With multiple replicas, a local invalidation\n * leaves peers stale until TTL unless cross-process invalidation is opted\n * into (`crossProcess: true`), which broadcasts over the database\n * adapter's notification capability (e.g. Postgres LISTEN/NOTIFY) when\n * the adapter provides one.\n *\n * Cached values are raw result rows, not hydrated instances — hydration and\n * read interceptors (tenancy, audit) run on every call, cached or not, and\n * each caller receives isolated row copies.\n *\n * Known limitations:\n * - Invalidation fires when a mutation's SQL executes, not when its\n * surrounding transaction commits. A write inside an uncommitted\n * transaction invalidates (and may broadcast) immediately; a concurrent\n * reader could repopulate the cache from the pre-commit snapshot, and a\n * rollback leaves the cache invalidated for a write that never landed.\n * Caching targets read-heavy / write-rare data where this is rare; for\n * models mutated inside multi-statement transactions, coherence is still\n * bounded by TTL. Per-call `cache: false` forces a fresh read where it\n * matters.\n * - Writes that bypass the framework's mutation paths (raw `db.query`\n * issued outside `collection.query()`, external processes without\n * `crossProcess`) are only bounded by TTL.\n *\n * @see https://github.com/happyvertical/smrt/issues/1498\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Read-through cache configuration for collection reads.\n *\n * Used both per call (`collection.list({ cache: { ttl: 60_000 } })`) and per\n * model (`@smrt({ cache: { ttl: 60_000 } })`).\n */\nexport interface CollectionCacheConfig {\n /**\n * Time-to-live for cached query results, in milliseconds. Must be > 0.\n */\n ttl: number;\n\n /**\n * Broadcast invalidations to peer processes through the database\n * adapter's notification capability (`db.notifications`, e.g. Postgres\n * LISTEN/NOTIFY). Without this, peer replicas serve stale rows until TTL.\n *\n * Model-level config (`@smrt({ cache })`) is the reliable opt-in: every\n * process that writes the model knows to broadcast. As a per-call option,\n * writes broadcast only from processes that have already performed a\n * `crossProcess` cached read of the same table (typical for homogeneous\n * replicas running the same routes); a process that only writes never\n * learns about the per-call opt-in, so its peers fall back to TTL expiry.\n *\n * Ignored (with a one-time warning) when the adapter does not expose\n * notifications.\n */\n crossProcess?: boolean;\n}\n\ninterface CacheEntry {\n expiresAt: number;\n rows: Record<string, unknown>[];\n}\n\n/**\n * Per-table entry cap. A table accumulating more distinct query shapes than\n * this within one TTL window evicts its oldest entries (insertion order).\n * Guards against unbounded growth from high-cardinality WHERE values.\n */\nconst MAX_ENTRIES_PER_TABLE = 500;\n\n/**\n * Notification channel used for cross-process invalidation broadcasts.\n */\nexport const CACHE_INVALIDATION_CHANNEL = 'smrt_collection_cache';\n\n/**\n * Identifies this process in broadcast payloads so a replica can skip\n * notifications it published itself (it already invalidated locally).\n *\n * Exported so sibling cross-replica buses (the #1763 change-signal bus) share\n * one per-process identity, keeping echo-avoidance consistent across channels.\n */\nexport const PROCESS_ID = crypto.randomUUID();\n\n/**\n * dbKey → tableName → (queryKey → entry)\n */\nconst store = new Map<string, Map<string, Map<string, CacheEntry>>>();\n\n/**\n * Monotonic invalidation generation per `dbKey\\0tableName`, bumped on every\n * invalidation. A read captures the generation *before* its DB round-trip and\n * passes it to `setCachedRows`; if an invalidating write landed during the\n * round-trip, the generation no longer matches and the (now-stale) result is\n * dropped instead of cached. This closes the read-miss/concurrent-write race\n * where an in-flight SELECT would otherwise repopulate the cache with\n * pre-write rows for the full TTL.\n */\nconst generations = new Map<string, number>();\n\nfunction generationKey(dbKey: string, tableName: string): string {\n return `${dbKey}\\0${tableName}`;\n}\n\n/**\n * Current invalidation generation for a table (0 if never invalidated).\n * Capture this before a DB read and pass it to `setCachedRows`.\n */\nexport function getCacheGeneration(dbKey: string, tableName: string): number {\n return generations.get(generationKey(dbKey, tableName)) ?? 0;\n}\n\n/**\n * Fallback identities for database instances that expose no URL\n * (each such instance gets its own scope, never shared).\n */\nconst fallbackDbKeys = new WeakMap<object, string>();\n\n/**\n * Resolve a stable cache scope for a database instance.\n *\n * Mirrors table-verifier's identity resolution (`db.url || config.url`).\n * `:memory:` databases share that URL string while being entirely separate\n * databases, so they (and URL-less instances) are scoped per instance —\n * serving one in-memory database's rows for another would be a correctness\n * bug, not just a stale read.\n */\nexport function resolveDbCacheKey(db: DatabaseInterface): string {\n const dbWithConfig = db as DatabaseInterface & {\n config?: { url?: string };\n };\n const url = db.url || dbWithConfig.config?.url;\n if (url && url !== ':memory:') return url;\n\n let key = fallbackDbKeys.get(db);\n if (!key) {\n key = `smrt-db:${crypto.randomUUID()}`;\n fallbackDbKeys.set(db, key);\n }\n return key;\n}\n\n/**\n * Build the cache key for a query. The final SQL and bound parameters fully\n * normalize the query shape — they already include STI discriminator\n * filters, interceptor-injected tenant filters, ORDER BY, LIMIT and OFFSET.\n */\nexport function buildQueryCacheKey(sql: string, params: unknown[]): string {\n return `${sql}\\0${JSON.stringify(params)}`;\n}\n\n/**\n * Read cached rows for a query, or undefined on miss/expiry.\n *\n * Returns a structured clone so callers can never mutate the cached copy\n * (hydration writes into row objects).\n */\nexport function getCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n): Record<string, unknown>[] | undefined {\n const entry = store.get(dbKey)?.get(tableName)?.get(queryKey);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n store.get(dbKey)?.get(tableName)?.delete(queryKey);\n return undefined;\n }\n return structuredClone(entry.rows);\n}\n\n/**\n * Store rows for a query under the table's cache scope.\n *\n * `expectedGeneration` is the value {@link getCacheGeneration} returned before\n * the DB read. If an invalidation bumped the table's generation while the read\n * was in flight, the result is stale and is dropped rather than cached.\n */\nexport function setCachedRows(\n dbKey: string,\n tableName: string,\n queryKey: string,\n rows: Record<string, unknown>[],\n ttl: number,\n expectedGeneration?: number,\n): void {\n if (!(ttl > 0)) return;\n\n // Drop a result that a concurrent write invalidated mid-flight.\n if (\n expectedGeneration !== undefined &&\n getCacheGeneration(dbKey, tableName) !== expectedGeneration\n ) {\n return;\n }\n\n let tables = store.get(dbKey);\n if (!tables) {\n tables = new Map();\n store.set(dbKey, tables);\n }\n let entries = tables.get(tableName);\n if (!entries) {\n entries = new Map();\n tables.set(tableName, entries);\n }\n\n // Evict oldest entries (Map preserves insertion order) at the cap. Skip when\n // refreshing an existing key — `set` updates it in place without growing the\n // map, so evicting first would drop a live entry and shrink the effective cap.\n if (!entries.has(queryKey)) {\n while (entries.size >= MAX_ENTRIES_PER_TABLE) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n }\n\n entries.set(queryKey, {\n expiresAt: Date.now() + ttl,\n rows: structuredClone(rows),\n });\n}\n\n/**\n * Drop every cached entry for a table. Called by the framework after any\n * successful mutation against that table — this is the write-invalidation\n * guarantee an app-side cache can't make.\n */\nexport function invalidateCollectionCache(\n dbKey: string,\n tableName: string,\n): void {\n store.get(dbKey)?.delete(tableName);\n // Bump the generation so any read whose DB round-trip is still in flight\n // discards its (now-stale) result instead of repopulating the table.\n const gkey = generationKey(dbKey, tableName);\n generations.set(gkey, (generations.get(gkey) ?? 0) + 1);\n}\n\n/**\n * Clear all cached collection reads and stop cross-process listeners.\n * Call in test setup to ensure isolation between test files.\n */\nexport function resetCollectionCache(): void {\n store.clear();\n generations.clear();\n crossProcessInterest.clear();\n stopCacheInvalidationListeners();\n}\n\n// ============================================================================\n// Per-call crossProcess interest (write-side broadcast decision)\n// ============================================================================\n\n/**\n * dbKey → tables this process has cached with a per-call `crossProcess`\n * opt-in. Writes consult this so per-call usage broadcasts too — model-level\n * config can't be the only trigger when the opt-in lives at the call site.\n */\nconst crossProcessInterest = new Map<string, Set<string>>();\n\n/**\n * Record that a per-call `crossProcess` cached read happened for a table.\n */\nexport function registerCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): void {\n let tables = crossProcessInterest.get(dbKey);\n if (!tables) {\n tables = new Set();\n crossProcessInterest.set(dbKey, tables);\n }\n tables.add(tableName);\n}\n\n/**\n * Whether any per-call `crossProcess` cached read has touched this table.\n */\nexport function hasCrossProcessCacheInterest(\n dbKey: string,\n tableName: string,\n): boolean {\n return crossProcessInterest.get(dbKey)?.has(tableName) ?? false;\n}\n\n// ============================================================================\n// Cross-process invalidation (opt-in, via db.notifications)\n// ============================================================================\n\ninterface ListenerHandle {\n iterator: AsyncIterator<unknown> | null;\n stopped: boolean;\n}\n\nconst listeners = new Map<string, ListenerHandle>();\nconst warnedNoNotifications = new Set<string>();\n\n/**\n * The database adapter's optional notification capability (e.g. Postgres\n * LISTEN/NOTIFY), or `undefined` when the adapter exposes none. Duck-typed so\n * core never depends on a concrete adapter shape. Exported so sibling\n * cross-replica buses (the #1763 change-signal bus) resolve it identically.\n */\nexport function getNotifications(db: DatabaseInterface) {\n return (db as DatabaseInterface & { notifications?: unknown })\n .notifications as\n | {\n notify(channel: string, payload: unknown): Promise<number>;\n listen(\n channel: string,\n options?: Record<string, unknown>,\n ): AsyncIterable<{ channel: string; payload: unknown }>;\n }\n | undefined;\n}\n\nfunction warnOnceNoNotifications(dbKey: string, context: string): void {\n if (warnedNoNotifications.has(dbKey)) return;\n warnedNoNotifications.add(dbKey);\n logger.warn(\n `Collection cache: crossProcess invalidation requested but the database ` +\n `adapter exposes no notification capability (${context}). Peer ` +\n `replicas will serve stale rows until TTL.`,\n );\n}\n\n/**\n * Broadcast a table invalidation to peer processes.\n *\n * Fire-and-forget from mutation paths: a broadcast failure must never fail\n * the write that triggered it.\n */\nexport async function broadcastCacheInvalidation(\n db: DatabaseInterface,\n tableName: string,\n): Promise<void> {\n const notifications = getNotifications(db);\n const dbKey = resolveDbCacheKey(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'broadcast');\n return;\n }\n try {\n await notifications.notify(CACHE_INVALIDATION_CHANNEL, {\n table: tableName,\n source: PROCESS_ID,\n });\n } catch (error) {\n logger.warn(\n `Collection cache: failed to broadcast invalidation for ${tableName}`,\n { error: error instanceof Error ? error.message : error },\n );\n }\n}\n\n/**\n * Ensure a background listener consumes invalidation broadcasts for this\n * database and drops matching local cache entries.\n *\n * Started lazily by the first cached read that opted into `crossProcess`.\n * Notifications published by this process are skipped — the local\n * invalidation already happened synchronously on the write path.\n */\nexport function ensureCacheInvalidationListener(db: DatabaseInterface): void {\n const dbKey = resolveDbCacheKey(db);\n if (listeners.has(dbKey)) return;\n\n const notifications = getNotifications(db);\n if (!notifications) {\n warnOnceNoNotifications(dbKey, 'listen');\n return;\n }\n\n const handle: ListenerHandle = { iterator: null, stopped: false };\n listeners.set(dbKey, handle);\n\n void (async () => {\n try {\n const iterable = notifications.listen(CACHE_INVALIDATION_CHANNEL);\n const iterator = iterable[Symbol.asyncIterator]();\n handle.iterator = iterator;\n\n while (!handle.stopped) {\n const { value, done } = await iterator.next();\n if (done || handle.stopped) break;\n\n const notification = value as { payload?: unknown };\n const payload: unknown =\n typeof notification?.payload === 'string'\n ? safeParse(notification.payload)\n : notification?.payload;\n\n if (!payload || typeof payload !== 'object') continue;\n const record = payload as { table?: unknown; source?: unknown };\n if (typeof record.table !== 'string') continue;\n if (record.source === PROCESS_ID) continue;\n\n invalidateCollectionCache(dbKey, record.table);\n }\n } catch (error) {\n if (!handle.stopped) {\n logger.warn(\n 'Collection cache: invalidation listener terminated unexpectedly; ' +\n 'cross-process invalidation is inactive for this database ' +\n '(local TTL still bounds staleness)',\n { error: error instanceof Error ? error.message : error },\n );\n }\n } finally {\n // Only retract our own handle. A concurrent stop+restart for the same\n // dbKey may have already installed a replacement; deleting unconditionally\n // would orphan it and leak/duplicate the live listener.\n if (listeners.get(dbKey) === handle) {\n listeners.delete(dbKey);\n }\n }\n })();\n}\n\n/**\n * Stop all cross-process invalidation listeners. Used by tests and during\n * shutdown; safe to call when none are active.\n */\nexport function stopCacheInvalidationListeners(): void {\n for (const handle of listeners.values()) {\n handle.stopped = true;\n void handle.iterator?.return?.(undefined);\n }\n listeners.clear();\n warnedNoNotifications.clear();\n}\n\nfunction safeParse(value: string): unknown {\n try {\n return JSON.parse(value);\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
|
package/dist/collection.d.ts
CHANGED
|
@@ -124,6 +124,23 @@ interface CollectionFieldDefinition {
|
|
|
124
124
|
* Configuration options for SmrtCollection
|
|
125
125
|
*/
|
|
126
126
|
export interface SmrtCollectionOptions extends SmrtClassOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Page size `list()` applies when the caller passes no `limit` (#2367).
|
|
129
|
+
*
|
|
130
|
+
* Unset by default — `list()` is the framework's bulk-read primitive and its
|
|
131
|
+
* internal callers expect every matching row, so an implicit default would
|
|
132
|
+
* truncate correct queries. Set this on collections whose reads are always
|
|
133
|
+
* paged.
|
|
134
|
+
*/
|
|
135
|
+
defaultListLimit?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Ceiling `list()` clamps every `limit` down to (#2367).
|
|
138
|
+
*
|
|
139
|
+
* Unset by default. The generated REST/SvelteKit/MCP surfaces enforce
|
|
140
|
+
* `MAX_LIST_LIMIT` on their own untrusted input regardless of this option;
|
|
141
|
+
* set it to extend the same ceiling to direct programmatic reads.
|
|
142
|
+
*/
|
|
143
|
+
maxListLimit?: number;
|
|
127
144
|
}
|
|
128
145
|
export type SmrtCollectionItemClass<ModelType extends SmrtObject> = (new (options: any) => ModelType) & {
|
|
129
146
|
create(options: any): ModelType | Promise<ModelType>;
|
|
@@ -171,9 +188,71 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
171
188
|
* @private
|
|
172
189
|
*/
|
|
173
190
|
private _cachedFields;
|
|
191
|
+
/**
|
|
192
|
+
* Opt-in list bounds (#2367). See {@link SmrtCollectionOptions.defaultListLimit}
|
|
193
|
+
* and {@link SmrtCollectionOptions.maxListLimit}.
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
private _defaultListLimit;
|
|
197
|
+
private _maxListLimit;
|
|
174
198
|
private getRegisteredItemClass;
|
|
175
199
|
private getResolvedItemClassName;
|
|
176
200
|
private getResolvedItemQualifiedName;
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the identifier whitelist and the sensitive-column blocklist that
|
|
203
|
+
* every caller-supplied identifier on this collection is checked against.
|
|
204
|
+
*
|
|
205
|
+
* Extracted from `convertWhereKeys()` so `orderBy` validation enforces the
|
|
206
|
+
* *same* sets (#2367). `where` (#1540) and `select` (#1902) both rejected
|
|
207
|
+
* sensitive columns while `orderBy` accepted any identifier, which made
|
|
208
|
+
* `?orderBy=api_secret&limit=1` an ordering oracle over a column the same
|
|
209
|
+
* request is forbidden to filter on or project.
|
|
210
|
+
*
|
|
211
|
+
* @returns the snake_case identifiers this collection accepts, the sensitive
|
|
212
|
+
* names it must refuse, and whether whitelist enforcement applies at all —
|
|
213
|
+
* classes with no registered fields (manifest-less inline test classes,
|
|
214
|
+
* #869) have nothing to validate against and fall through to SQL.
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
private collectQueryableFieldNames;
|
|
218
|
+
/**
|
|
219
|
+
* Build the `ORDER BY` clause for `list()`, validating every term against the
|
|
220
|
+
* same whitelist and sensitive-column blocklist as `where` and `select`
|
|
221
|
+
* (#2367).
|
|
222
|
+
*
|
|
223
|
+
* `orderBy` is interpolated UNPARAMETERIZED into the identifier position, and
|
|
224
|
+
* it arrives from the generated REST/MCP/SvelteKit surfaces as caller input.
|
|
225
|
+
* The identifier regex here has always blocked injection; what it did not
|
|
226
|
+
* block was ordering by a column the caller may not read. Sorting is a
|
|
227
|
+
* comparison, and a comparison against a secret is an oracle: repeatedly
|
|
228
|
+
* requesting `?orderBy=api_secret&limit=1` (with a shrinking `where`) walks
|
|
229
|
+
* the secret's value ordering without ever serializing the column.
|
|
230
|
+
*
|
|
231
|
+
* @param orderBy - one `'<field> [ASC|DESC]'` term or an array of them
|
|
232
|
+
* @param fields - the collection's cached field definitions
|
|
233
|
+
* @returns the `' ORDER BY ...'` fragment, or `''` when no ordering was asked
|
|
234
|
+
* for
|
|
235
|
+
* @throws {QueryOrderByError} on a malformed term, an unknown column, or a
|
|
236
|
+
* sensitive column — all rendered as a 400 by the generated surfaces
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
private buildOrderBySql;
|
|
240
|
+
/**
|
|
241
|
+
* Apply this collection's configured list bounds (#2367).
|
|
242
|
+
*
|
|
243
|
+
* Both bounds are opt-in (`defaultListLimit` / `maxListLimit` collection
|
|
244
|
+
* options) and default to unset. `list()` is the framework's bulk-read
|
|
245
|
+
* primitive — relationship loaders, junction hydration and `listByIds()` all
|
|
246
|
+
* go through it expecting every matching row — so a framework-wide implicit
|
|
247
|
+
* default would silently truncate correct queries instead of bounding an
|
|
248
|
+
* attack surface. The generated surfaces, where untrusted input actually
|
|
249
|
+
* arrives, apply {@link DEFAULT_LIST_LIMIT}/{@link MAX_LIST_LIMIT}
|
|
250
|
+
* unconditionally; an application that wants the same ceiling on its own
|
|
251
|
+
* programmatic reads sets `maxListLimit` when constructing the collection.
|
|
252
|
+
*
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
private applyListBounds;
|
|
177
256
|
/**
|
|
178
257
|
* Convert WHERE clause field names from camelCase to snake_case while preserving operators.
|
|
179
258
|
* Validates operators and field names to prevent SQL injection and invalid queries.
|
|
@@ -197,6 +276,16 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
197
276
|
private isSensitiveFieldDefinition;
|
|
198
277
|
private getReadPermissionFieldDefinition;
|
|
199
278
|
private collectSensitiveFieldNames;
|
|
279
|
+
/**
|
|
280
|
+
* The `collectSensitiveFieldNames` sibling for `@field({ readPermission })`
|
|
281
|
+
* columns (#2367).
|
|
282
|
+
*
|
|
283
|
+
* These are per-caller redacted by `toPublicJSON()` and refused outright by
|
|
284
|
+
* `select`, so an ordering over one leaks comparisons about a value the
|
|
285
|
+
* caller may not read. `list()` has no permission context, so — exactly as
|
|
286
|
+
* `select` does — they are refused unconditionally rather than conditionally.
|
|
287
|
+
*/
|
|
288
|
+
private collectReadPermissionFieldNames;
|
|
200
289
|
private hasCustomPrimaryKey;
|
|
201
290
|
private isOmittedCustomPrimaryKeySystemField;
|
|
202
291
|
private resolveProjectionSelect;
|
|
@@ -260,7 +349,7 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
260
349
|
* });
|
|
261
350
|
* ```
|
|
262
351
|
*/
|
|
263
|
-
static create<T extends SmrtCollection<any>>(this: new (options?: SmrtCollectionOptions) => T, options?:
|
|
352
|
+
static create<T extends SmrtCollection<any>>(this: new (options?: SmrtCollectionOptions) => T, options?: SmrtCollectionOptions): Promise<T>;
|
|
264
353
|
/**
|
|
265
354
|
* Async initialization hook for the collection.
|
|
266
355
|
*
|
|
@@ -337,8 +426,18 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
337
426
|
* This is a convenience method that avoids N+1 queries when you have
|
|
338
427
|
* a list of IDs and need to fetch the corresponding records.
|
|
339
428
|
*
|
|
429
|
+
* The id list is chunked at `IN_LIST_CHUNK_SIZE` (#2367). `collection.list()`
|
|
430
|
+
* expands an array-valued WHERE into one `id IN (?, ?, ...)` clause and does
|
|
431
|
+
* not chunk it, so an unbounded caller-sized array hits the backend's
|
|
432
|
+
* bind-variable ceiling — `SQLITE_MAX_VARIABLE_NUMBER` (999 pre-3.32, 32766
|
|
433
|
+
* after) or PostgreSQL's 65535 — and the whole query fails before it runs.
|
|
434
|
+
* The relationship, junction and hierarchy loaders have chunked at this value
|
|
435
|
+
* for exactly this reason; `listByIds()` is the remaining path that took ids
|
|
436
|
+
* straight from the caller and did not.
|
|
437
|
+
*
|
|
340
438
|
* @param ids - Array of UUIDs to fetch
|
|
341
|
-
* @returns Promise resolving to array of objects (order not guaranteed
|
|
439
|
+
* @returns Promise resolving to array of objects (order not guaranteed;
|
|
440
|
+
* results are concatenated in chunk order)
|
|
342
441
|
*
|
|
343
442
|
* @example
|
|
344
443
|
* ```typescript
|
|
@@ -747,12 +846,36 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
747
846
|
* model hook without relying on driver queuing and preserves row order.
|
|
748
847
|
*/
|
|
749
848
|
private hydrateResultRows;
|
|
849
|
+
/**
|
|
850
|
+
* Resolve the `_smrt_contexts.owner_id` key for collection-level memory
|
|
851
|
+
* (#2365).
|
|
852
|
+
*
|
|
853
|
+
* `_smrt_contexts` has no tenant column, so before this hook a tenant-scoped
|
|
854
|
+
* collection's learned memory (`remember()`/`recall()`) was filed under one
|
|
855
|
+
* shared `__collection__` key and leaked across tenants. For a tenant-scoped
|
|
856
|
+
* item class under an active tenant context the key becomes
|
|
857
|
+
* `__collection__:<tenantId>`; everything else (tenancy disabled, no active
|
|
858
|
+
* tenant, system context, non-tenant-scoped class) keeps the shared
|
|
859
|
+
* `__collection__` key. Tenant-keyed memory is strictly isolated — a tenant
|
|
860
|
+
* recall does NOT fall back to the shared key, so memory learned outside a
|
|
861
|
+
* tenant context is invisible inside one (and vice versa).
|
|
862
|
+
*
|
|
863
|
+
* Tenant resolution uses the same core-side trust anchors as the generated
|
|
864
|
+
* REST read scope (#1782): `resolveDispatchTenantScope()` for the active
|
|
865
|
+
* tenant and the triple tenant-scoped-class check covering `@smrt()` config,
|
|
866
|
+
* manifest config, and the tenancy-filled `@TenantScoped()` resolver.
|
|
867
|
+
*/
|
|
868
|
+
private resolveCollectionMemoryOwnerId;
|
|
750
869
|
/**
|
|
751
870
|
* Remember collection-level context
|
|
752
871
|
*
|
|
753
872
|
* Stores context applicable to all instances of this collection type.
|
|
754
873
|
* Use for patterns that apply to the entire collection (e.g., default parsing strategies).
|
|
755
874
|
*
|
|
875
|
+
* Under an active tenant context on a tenant-scoped class, memory is keyed
|
|
876
|
+
* per tenant and never shared across tenants (#2365) — see
|
|
877
|
+
* {@link resolveCollectionMemoryOwnerId}.
|
|
878
|
+
*
|
|
756
879
|
* `expiresAt` is stored on the `_smrt_contexts` row as caller-managed metadata.
|
|
757
880
|
* {@link recall} and {@link recallAll} do **not** filter on it, so an expired
|
|
758
881
|
* entry is still returned; filter or delete expired entries yourself.
|
|
@@ -879,6 +1002,11 @@ export declare class SmrtCollection<ModelType extends SmrtObject> extends SmrtCl
|
|
|
879
1002
|
* Generates an embedding for the query text and finds similar objects
|
|
880
1003
|
* based on cosine similarity of stored embeddings.
|
|
881
1004
|
*
|
|
1005
|
+
* Under an active tenant context on a tenant-scoped class, candidates are
|
|
1006
|
+
* restricted to the tenant's rows BEFORE top-K ranking (#2365), so results
|
|
1007
|
+
* are never starved by — and similarity ranks never leak — other tenants'
|
|
1008
|
+
* content.
|
|
1009
|
+
*
|
|
882
1010
|
* @param query - Text to search for
|
|
883
1011
|
* @param options - Search options
|
|
884
1012
|
* @param options.field - Specific field to search (defaults to first embedding
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAEL,KAAK,qBAAqB,EAQ3B,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
|