@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
package/dist/collection.js
CHANGED
|
@@ -1,18 +1,61 @@
|
|
|
1
|
+
import { chunkArray } from "./utils/chunk.js";
|
|
2
|
+
import { classnameToTablename, toSnakeCase } from "./utils/naming.js";
|
|
1
3
|
import { buildQueryCacheKey, ensureCacheInvalidationListener, getCacheGeneration, getCachedRows, invalidateCollectionCache, registerCrossProcessCacheInterest, resolveDbCacheKey, setCachedRows } from "./collection-cache.js";
|
|
2
|
-
import {
|
|
4
|
+
import { isTenantScopedClassResolved, resolveDispatchTenantScope } from "./dispatch/tenant-resolver.js";
|
|
5
|
+
import { GlobalInterceptors, createInterceptorContext, resolveGetStringFilter } from "./interceptors.js";
|
|
3
6
|
import { SmrtClass } from "./class.js";
|
|
4
7
|
import { EmbeddingProvider } from "./embeddings/provider.js";
|
|
5
8
|
import { EmbeddingStorage } from "./embeddings/storage.js";
|
|
6
|
-
import {
|
|
9
|
+
import { QueryBoundsError, QueryOrderByError } from "./query-bounds.js";
|
|
7
10
|
import { verifyPersistenceTable } from "./schema/table-verifier.js";
|
|
8
11
|
import { fieldsFromClass, formatDataJs, toCamelCase } from "./utils.js";
|
|
9
12
|
import { ObjectRegistry } from "./registry.js";
|
|
10
|
-
import { chunkArray } from "./utils/chunk.js";
|
|
11
13
|
import { createLogger } from "@happyvertical/logger";
|
|
12
14
|
import { buildWhere } from "@happyvertical/sql";
|
|
13
15
|
//#region src/collection.ts
|
|
14
16
|
var logger = createLogger({ level: "info" });
|
|
15
17
|
/**
|
|
18
|
+
* `_smrt_contexts.owner_id` value for collection-level (as opposed to
|
|
19
|
+
* per-object) memory. Tenant-scoped collections suffix the active tenant id
|
|
20
|
+
* (`__collection__:<tenantId>`) so learned memory never crosses tenants —
|
|
21
|
+
* see {@link SmrtCollection.resolveCollectionMemoryOwnerId} (#2365).
|
|
22
|
+
*/
|
|
23
|
+
var COLLECTION_MEMORY_OWNER_ID = "__collection__";
|
|
24
|
+
/**
|
|
25
|
+
* Validate an optional collection-level list bound (#2367).
|
|
26
|
+
*
|
|
27
|
+
* @returns the bound, or `undefined` when the option was not supplied
|
|
28
|
+
* @throws {QueryBoundsError} when the option is present but not a positive
|
|
29
|
+
* integer — a `maxListLimit` of `0` or `-1` would silently make every read on
|
|
30
|
+
* the collection return nothing
|
|
31
|
+
*/
|
|
32
|
+
function assertOptionalListBound(value, optionName) {
|
|
33
|
+
if (value === void 0) return void 0;
|
|
34
|
+
if (!Number.isSafeInteger(value) || value < 1) throw new QueryBoundsError(`Invalid ${optionName}: expected a positive integer, got ${String(value)}.`, "INVALID_QUERY_BOUNDS", {
|
|
35
|
+
optionName,
|
|
36
|
+
value
|
|
37
|
+
});
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate a per-query `limit`/`offset` before it is bound (#2367).
|
|
42
|
+
*
|
|
43
|
+
* `0` is allowed — `LIMIT 0` and `OFFSET 0` are both meaningful — but `NaN`,
|
|
44
|
+
* `Infinity`, negatives and fractions are not: they used to be bound verbatim
|
|
45
|
+
* and surface as a driver-level 500 for what is a caller error.
|
|
46
|
+
*
|
|
47
|
+
* @returns the bound, or `undefined` when the caller supplied none
|
|
48
|
+
* @throws {QueryBoundsError} when the value is not a non-negative integer
|
|
49
|
+
*/
|
|
50
|
+
function assertQueryBound(value, parameterName) {
|
|
51
|
+
if (value === void 0 || value === null) return void 0;
|
|
52
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new QueryBoundsError(`Invalid ${parameterName}: expected a non-negative integer, got ${String(value)}.`, "INVALID_QUERY_BOUNDS", {
|
|
53
|
+
parameterName,
|
|
54
|
+
value
|
|
55
|
+
});
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
16
59
|
* Resolve _meta_type in WHERE clause from simple class name to qualified name (Issue #713)
|
|
17
60
|
*
|
|
18
61
|
* This helper function allows queries like { _meta_type: 'Image' } to work correctly
|
|
@@ -91,6 +134,13 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
91
134
|
* @private
|
|
92
135
|
*/
|
|
93
136
|
_cachedFields = null;
|
|
137
|
+
/**
|
|
138
|
+
* Opt-in list bounds (#2367). See {@link SmrtCollectionOptions.defaultListLimit}
|
|
139
|
+
* and {@link SmrtCollectionOptions.maxListLimit}.
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_defaultListLimit;
|
|
143
|
+
_maxListLimit;
|
|
94
144
|
getRegisteredItemClass() {
|
|
95
145
|
return ObjectRegistry.getClassByConstructor(this._itemClass) || ObjectRegistry.getClass(this._itemClass.name);
|
|
96
146
|
}
|
|
@@ -102,6 +152,135 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
102
152
|
return registered?.qualifiedName || registered?.name || this._itemClass.name;
|
|
103
153
|
}
|
|
104
154
|
/**
|
|
155
|
+
* Resolve the identifier whitelist and the sensitive-column blocklist that
|
|
156
|
+
* every caller-supplied identifier on this collection is checked against.
|
|
157
|
+
*
|
|
158
|
+
* Extracted from `convertWhereKeys()` so `orderBy` validation enforces the
|
|
159
|
+
* *same* sets (#2367). `where` (#1540) and `select` (#1902) both rejected
|
|
160
|
+
* sensitive columns while `orderBy` accepted any identifier, which made
|
|
161
|
+
* `?orderBy=api_secret&limit=1` an ordering oracle over a column the same
|
|
162
|
+
* request is forbidden to filter on or project.
|
|
163
|
+
*
|
|
164
|
+
* @returns the snake_case identifiers this collection accepts, the sensitive
|
|
165
|
+
* names it must refuse, and whether whitelist enforcement applies at all —
|
|
166
|
+
* classes with no registered fields (manifest-less inline test classes,
|
|
167
|
+
* #869) have nothing to validate against and fall through to SQL.
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
collectQueryableFieldNames(fields) {
|
|
171
|
+
const validFieldNames = new Set(Object.keys(fields).map((f) => toSnakeCase(f)));
|
|
172
|
+
const sensitiveFieldNames = this.collectSensitiveFieldNames(fields);
|
|
173
|
+
const readPermissionFieldNames = this.collectReadPermissionFieldNames(fields);
|
|
174
|
+
validFieldNames.add("id");
|
|
175
|
+
validFieldNames.add("slug");
|
|
176
|
+
validFieldNames.add("context");
|
|
177
|
+
validFieldNames.add("created_at");
|
|
178
|
+
validFieldNames.add("updated_at");
|
|
179
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
180
|
+
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
181
|
+
if (ObjectRegistry.getTableStrategy(itemQualifiedName) === "sti") {
|
|
182
|
+
validFieldNames.add("_meta_type");
|
|
183
|
+
validFieldNames.add("meta_type");
|
|
184
|
+
validFieldNames.add("_meta_data");
|
|
185
|
+
validFieldNames.add("meta_data");
|
|
186
|
+
const inheritanceChain = ObjectRegistry.getInheritanceChain(itemQualifiedName);
|
|
187
|
+
for (const ancestorName of inheritanceChain) {
|
|
188
|
+
if (ancestorName === "SmrtObject" || ancestorName === "SmrtClass" || ancestorName === itemQualifiedName || ancestorName === itemClassName) continue;
|
|
189
|
+
const ancestorFields = ObjectRegistry.getFields(ancestorName);
|
|
190
|
+
for (const fieldName of ancestorFields.keys()) validFieldNames.add(toSnakeCase(fieldName));
|
|
191
|
+
this.collectSensitiveFieldNames(ancestorFields, sensitiveFieldNames);
|
|
192
|
+
this.collectReadPermissionFieldNames(ancestorFields, readPermissionFieldNames);
|
|
193
|
+
}
|
|
194
|
+
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
195
|
+
if (stiBase) for (const descendant of ObjectRegistry.getDescendants(stiBase)) {
|
|
196
|
+
const descendantFields = ObjectRegistry.getFields(descendant);
|
|
197
|
+
this.collectSensitiveFieldNames(descendantFields, sensitiveFieldNames);
|
|
198
|
+
this.collectReadPermissionFieldNames(descendantFields, readPermissionFieldNames);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
readPermissionFieldNames,
|
|
203
|
+
skipFieldValidation: Object.keys(fields).length === 0,
|
|
204
|
+
sensitiveFieldNames,
|
|
205
|
+
validFieldNames
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Build the `ORDER BY` clause for `list()`, validating every term against the
|
|
210
|
+
* same whitelist and sensitive-column blocklist as `where` and `select`
|
|
211
|
+
* (#2367).
|
|
212
|
+
*
|
|
213
|
+
* `orderBy` is interpolated UNPARAMETERIZED into the identifier position, and
|
|
214
|
+
* it arrives from the generated REST/MCP/SvelteKit surfaces as caller input.
|
|
215
|
+
* The identifier regex here has always blocked injection; what it did not
|
|
216
|
+
* block was ordering by a column the caller may not read. Sorting is a
|
|
217
|
+
* comparison, and a comparison against a secret is an oracle: repeatedly
|
|
218
|
+
* requesting `?orderBy=api_secret&limit=1` (with a shrinking `where`) walks
|
|
219
|
+
* the secret's value ordering without ever serializing the column.
|
|
220
|
+
*
|
|
221
|
+
* @param orderBy - one `'<field> [ASC|DESC]'` term or an array of them
|
|
222
|
+
* @param fields - the collection's cached field definitions
|
|
223
|
+
* @returns the `' ORDER BY ...'` fragment, or `''` when no ordering was asked
|
|
224
|
+
* for
|
|
225
|
+
* @throws {QueryOrderByError} on a malformed term, an unknown column, or a
|
|
226
|
+
* sensitive column — all rendered as a 400 by the generated surfaces
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
buildOrderBySql(orderBy, fields) {
|
|
230
|
+
if (!orderBy) return "";
|
|
231
|
+
const orderByItems = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
232
|
+
if (orderByItems.length === 0) return "";
|
|
233
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
234
|
+
const { readPermissionFieldNames, sensitiveFieldNames, skipFieldValidation, validFieldNames } = this.collectQueryableFieldNames(fields);
|
|
235
|
+
const definitionByColumn = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const [fieldName, fieldDef] of Object.entries(fields)) definitionByColumn.set(toSnakeCase(fieldName), fieldDef);
|
|
237
|
+
const customPrimaryKey = this.hasCustomPrimaryKey(fields);
|
|
238
|
+
const registeredFields = ObjectRegistry.getFields(this.getResolvedItemQualifiedName());
|
|
239
|
+
const explicitFieldNames = new Set((registeredFields.size > 0 ? registeredFields : ObjectRegistry.getFields(itemClassName)).keys());
|
|
240
|
+
return ` ORDER BY ${orderByItems.map((item) => {
|
|
241
|
+
const [field, direction = "ASC"] = String(item).trim().split(/\s+/);
|
|
242
|
+
if (!/^[a-zA-Z0-9_]+$/.test(field)) throw new QueryOrderByError(`Invalid field name for ordering: ${field}`, `Invalid field name for ordering: ${field}`, "INVALID_ORDER_BY", { field });
|
|
243
|
+
const normalizedDirection = direction.toUpperCase();
|
|
244
|
+
if (normalizedDirection !== "ASC" && normalizedDirection !== "DESC") throw new QueryOrderByError(`Invalid sort direction: ${direction}. Must be ASC or DESC.`, `Invalid sort direction: ${direction}. Must be ASC or DESC.`, "INVALID_ORDER_BY", { direction });
|
|
245
|
+
const strippedName = toSnakeCase(field);
|
|
246
|
+
const columnName = definitionByColumn.has(strippedName) ? strippedName : this.toDbColumnName(field);
|
|
247
|
+
if (sensitiveFieldNames.has(columnName) || sensitiveFieldNames.has(field)) throw new QueryOrderByError(`Invalid orderBy field: '${field}'. Ordering by sensitive fields is not allowed.`, `Invalid orderBy field: '${field}'. Ordering by sensitive fields is not allowed.`, "INVALID_ORDER_BY_SENSITIVE", { field });
|
|
248
|
+
if (readPermissionFieldNames.has(columnName) || readPermissionFieldNames.has(field)) throw new QueryOrderByError(`Invalid orderBy field: '${field}'. Ordering by permission-gated fields is not allowed.`, `Invalid orderBy field: '${field}'. Ordering by permission-gated fields is not allowed.`, "INVALID_ORDER_BY_RESTRICTED", { field });
|
|
249
|
+
if (!skipFieldValidation && !validFieldNames.has(columnName)) throw new QueryOrderByError(`Invalid orderBy field: '${field}'. Field does not exist on ${itemClassName}. Valid fields: ${Array.from(validFieldNames).sort().join(", ")}`, `Invalid orderBy field: '${field}'.`, "INVALID_ORDER_BY_UNKNOWN_FIELD", {
|
|
250
|
+
field,
|
|
251
|
+
itemClassName
|
|
252
|
+
});
|
|
253
|
+
const fieldDef = definitionByColumn.get(columnName);
|
|
254
|
+
const fieldType = fieldDef?.type;
|
|
255
|
+
const isTransient = fieldDef?.transient === true || fieldDef?._meta?.transient === true;
|
|
256
|
+
if (fieldType === "meta" || fieldType === "oneToMany" || fieldType === "manyToMany" || isTransient || this.isOmittedCustomPrimaryKeySystemField(columnName, customPrimaryKey, explicitFieldNames)) throw new QueryOrderByError(`Invalid orderBy field: '${field}'. Field is not column-backed on ${itemClassName}.`, `Invalid orderBy field: '${field}'. Field is not column-backed.`, "INVALID_ORDER_BY_NOT_COLUMN_BACKED", {
|
|
257
|
+
field,
|
|
258
|
+
itemClassName
|
|
259
|
+
});
|
|
260
|
+
return `${columnName} ${normalizedDirection}`;
|
|
261
|
+
}).join(", ")}`;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Apply this collection's configured list bounds (#2367).
|
|
265
|
+
*
|
|
266
|
+
* Both bounds are opt-in (`defaultListLimit` / `maxListLimit` collection
|
|
267
|
+
* options) and default to unset. `list()` is the framework's bulk-read
|
|
268
|
+
* primitive — relationship loaders, junction hydration and `listByIds()` all
|
|
269
|
+
* go through it expecting every matching row — so a framework-wide implicit
|
|
270
|
+
* default would silently truncate correct queries instead of bounding an
|
|
271
|
+
* attack surface. The generated surfaces, where untrusted input actually
|
|
272
|
+
* arrives, apply {@link DEFAULT_LIST_LIMIT}/{@link MAX_LIST_LIMIT}
|
|
273
|
+
* unconditionally; an application that wants the same ceiling on its own
|
|
274
|
+
* programmatic reads sets `maxListLimit` when constructing the collection.
|
|
275
|
+
*
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
applyListBounds(limit) {
|
|
279
|
+
const effective = limit ?? this._defaultListLimit;
|
|
280
|
+
if (effective === void 0) return void 0;
|
|
281
|
+
return this._maxListLimit === void 0 ? effective : Math.min(effective, this._maxListLimit);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
105
284
|
* Convert WHERE clause field names from camelCase to snake_case while preserving operators.
|
|
106
285
|
* Validates operators and field names to prevent SQL injection and invalid queries.
|
|
107
286
|
*
|
|
@@ -131,31 +310,8 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
131
310
|
];
|
|
132
311
|
const UNSUPPORTED_OPERATOR_HINTS = /* @__PURE__ */ new Map([["contains", "Use 'like' with explicit wildcards instead, e.g. { 'name like': '%term%' }."]]);
|
|
133
312
|
const fields = this.getFieldsSync();
|
|
134
|
-
const validFieldNames = new Set(Object.keys(fields).map((f) => toSnakeCase(f)));
|
|
135
|
-
const sensitiveFieldNames = this.collectSensitiveFieldNames(fields);
|
|
136
|
-
validFieldNames.add("id");
|
|
137
|
-
validFieldNames.add("slug");
|
|
138
|
-
validFieldNames.add("context");
|
|
139
|
-
validFieldNames.add("created_at");
|
|
140
|
-
validFieldNames.add("updated_at");
|
|
141
313
|
const itemClassName = this.getResolvedItemClassName();
|
|
142
|
-
const
|
|
143
|
-
if (ObjectRegistry.getTableStrategy(itemQualifiedName) === "sti") {
|
|
144
|
-
validFieldNames.add("_meta_type");
|
|
145
|
-
validFieldNames.add("meta_type");
|
|
146
|
-
validFieldNames.add("_meta_data");
|
|
147
|
-
validFieldNames.add("meta_data");
|
|
148
|
-
const inheritanceChain = ObjectRegistry.getInheritanceChain(itemQualifiedName);
|
|
149
|
-
for (const ancestorName of inheritanceChain) {
|
|
150
|
-
if (ancestorName === "SmrtObject" || ancestorName === "SmrtClass" || ancestorName === itemQualifiedName || ancestorName === itemClassName) continue;
|
|
151
|
-
const ancestorFields = ObjectRegistry.getFields(ancestorName);
|
|
152
|
-
for (const fieldName of ancestorFields.keys()) validFieldNames.add(toSnakeCase(fieldName));
|
|
153
|
-
this.collectSensitiveFieldNames(ancestorFields, sensitiveFieldNames);
|
|
154
|
-
}
|
|
155
|
-
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
156
|
-
if (stiBase) for (const descendant of ObjectRegistry.getDescendants(stiBase)) this.collectSensitiveFieldNames(ObjectRegistry.getFields(descendant), sensitiveFieldNames);
|
|
157
|
-
}
|
|
158
|
-
const skipFieldValidation = !(Object.keys(fields).length > 0);
|
|
314
|
+
const { sensitiveFieldNames, skipFieldValidation, validFieldNames } = this.collectQueryableFieldNames(fields);
|
|
159
315
|
const converted = {};
|
|
160
316
|
if (Object.hasOwn(where, "__proto__") || Object.hasOwn(where, "constructor") || Object.hasOwn(where, "prototype")) throw new Error("Invalid WHERE clause: Prototype pollution attempts are not allowed. Detected dangerous properties in WHERE clause.");
|
|
161
317
|
for (const [key, value] of Object.entries(where)) {
|
|
@@ -212,6 +368,23 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
212
368
|
}
|
|
213
369
|
return target;
|
|
214
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* The `collectSensitiveFieldNames` sibling for `@field({ readPermission })`
|
|
373
|
+
* columns (#2367).
|
|
374
|
+
*
|
|
375
|
+
* These are per-caller redacted by `toPublicJSON()` and refused outright by
|
|
376
|
+
* `select`, so an ordering over one leaks comparisons about a value the
|
|
377
|
+
* caller may not read. `list()` has no permission context, so — exactly as
|
|
378
|
+
* `select` does — they are refused unconditionally rather than conditionally.
|
|
379
|
+
*/
|
|
380
|
+
collectReadPermissionFieldNames(fieldMap, target = /* @__PURE__ */ new Set()) {
|
|
381
|
+
const entries = fieldMap instanceof Map ? fieldMap.entries() : Object.entries(fieldMap);
|
|
382
|
+
for (const [fieldName, fieldDef] of entries) if (this.getReadPermissionFieldDefinition(fieldDef) !== void 0) {
|
|
383
|
+
target.add(toSnakeCase(fieldName));
|
|
384
|
+
target.add(fieldName);
|
|
385
|
+
}
|
|
386
|
+
return target;
|
|
387
|
+
}
|
|
215
388
|
hasCustomPrimaryKey(fields) {
|
|
216
389
|
return Object.values(fields).some((fieldDef) => fieldDef.primaryKey === true || fieldDef._meta?.primaryKey === true);
|
|
217
390
|
}
|
|
@@ -337,6 +510,8 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
337
510
|
*/
|
|
338
511
|
constructor(options = {}) {
|
|
339
512
|
super(options);
|
|
513
|
+
this._defaultListLimit = assertOptionalListBound(options.defaultListLimit, "defaultListLimit");
|
|
514
|
+
this._maxListLimit = assertOptionalListBound(options.maxListLimit, "maxListLimit");
|
|
340
515
|
const collectionCtor = this.constructor;
|
|
341
516
|
if (this.constructor !== SmrtCollection && collectionCtor._itemClass) {
|
|
342
517
|
const itemClass = collectionCtor._itemClass;
|
|
@@ -370,14 +545,16 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
370
545
|
* ```
|
|
371
546
|
*/
|
|
372
547
|
static async create(options = {}) {
|
|
373
|
-
const { _className, db, persistence, ai, fs, logging, metrics, pubsub, sanitization, signals } = options;
|
|
548
|
+
const { _className, db, defaultListLimit, persistence, ai, fs, logging, maxListLimit, metrics, pubsub, sanitization, signals } = options;
|
|
374
549
|
const collectionOptions = {
|
|
375
550
|
_className,
|
|
376
551
|
db,
|
|
377
552
|
persistence,
|
|
378
553
|
ai,
|
|
554
|
+
defaultListLimit,
|
|
379
555
|
fs,
|
|
380
556
|
logging,
|
|
557
|
+
maxListLimit,
|
|
381
558
|
metrics,
|
|
382
559
|
pubsub,
|
|
383
560
|
sanitization,
|
|
@@ -472,8 +649,18 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
472
649
|
* This is a convenience method that avoids N+1 queries when you have
|
|
473
650
|
* a list of IDs and need to fetch the corresponding records.
|
|
474
651
|
*
|
|
652
|
+
* The id list is chunked at `IN_LIST_CHUNK_SIZE` (#2367). `collection.list()`
|
|
653
|
+
* expands an array-valued WHERE into one `id IN (?, ?, ...)` clause and does
|
|
654
|
+
* not chunk it, so an unbounded caller-sized array hits the backend's
|
|
655
|
+
* bind-variable ceiling — `SQLITE_MAX_VARIABLE_NUMBER` (999 pre-3.32, 32766
|
|
656
|
+
* after) or PostgreSQL's 65535 — and the whole query fails before it runs.
|
|
657
|
+
* The relationship, junction and hierarchy loaders have chunked at this value
|
|
658
|
+
* for exactly this reason; `listByIds()` is the remaining path that took ids
|
|
659
|
+
* straight from the caller and did not.
|
|
660
|
+
*
|
|
475
661
|
* @param ids - Array of UUIDs to fetch
|
|
476
|
-
* @returns Promise resolving to array of objects (order not guaranteed
|
|
662
|
+
* @returns Promise resolving to array of objects (order not guaranteed;
|
|
663
|
+
* results are concatenated in chunk order)
|
|
477
664
|
*
|
|
478
665
|
* @example
|
|
479
666
|
* ```typescript
|
|
@@ -482,7 +669,17 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
482
669
|
*/
|
|
483
670
|
async listByIds(ids) {
|
|
484
671
|
if (ids.length === 0) return [];
|
|
485
|
-
|
|
672
|
+
const chunks = chunkArray(ids, this._maxListLimit === void 0 ? 900 : Math.min(900, this._maxListLimit));
|
|
673
|
+
if (chunks.length === 1) return this.list({
|
|
674
|
+
limit: chunks[0].length,
|
|
675
|
+
where: { id: chunks[0] }
|
|
676
|
+
});
|
|
677
|
+
const results = [];
|
|
678
|
+
for (const idChunk of chunks) results.push(...await this.list({
|
|
679
|
+
limit: idChunk.length,
|
|
680
|
+
where: { id: idChunk }
|
|
681
|
+
}));
|
|
682
|
+
return results;
|
|
486
683
|
}
|
|
487
684
|
/**
|
|
488
685
|
* Resolve the effective cache config for a read (issue #1498).
|
|
@@ -563,10 +760,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
563
760
|
const itemQualifiedName = this.getResolvedItemQualifiedName();
|
|
564
761
|
const interceptorContext = createInterceptorContext(itemClassName, "get", this.constructor.name);
|
|
565
762
|
const interceptedFilter = await GlobalInterceptors.executeBeforeGet(itemClassName, filter, interceptorContext);
|
|
566
|
-
let where = typeof interceptedFilter === "string" ?
|
|
567
|
-
slug: interceptedFilter,
|
|
568
|
-
context: ""
|
|
569
|
-
} : interceptedFilter;
|
|
763
|
+
let where = typeof interceptedFilter === "string" ? resolveGetStringFilter(interceptedFilter) : interceptedFilter;
|
|
570
764
|
const isSTI = ObjectRegistry.getTableStrategy(itemQualifiedName) === "sti";
|
|
571
765
|
if (isSTI) {
|
|
572
766
|
const stiBase = ObjectRegistry.getSTIBase(itemQualifiedName);
|
|
@@ -576,7 +770,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
576
770
|
};
|
|
577
771
|
}
|
|
578
772
|
const { sql: whereSql, values: whereValues } = buildWhere(this.convertWhereKeys(where));
|
|
579
|
-
const fullSQL = `SELECT * FROM ${this.tableName} ${whereSql}`;
|
|
773
|
+
const fullSQL = `SELECT * FROM ${this.tableName} ${whereSql} LIMIT 1`;
|
|
580
774
|
const rows = await this.queryRowsWithCache(fullSQL, whereValues, this.resolveReadCacheConfig(options.cache));
|
|
581
775
|
if (!rows?.[0]) return await GlobalInterceptors.executeAfterGet(itemClassName, null, interceptorContext);
|
|
582
776
|
const fields = this.getFieldsSync();
|
|
@@ -603,27 +797,19 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
603
797
|
const fields = this.getFieldsSync();
|
|
604
798
|
const projection = select !== void 0 ? this.resolveProjectionSelect(select, fields, isSTI) : void 0;
|
|
605
799
|
if (projection && interceptedOptions.include && interceptedOptions.include.length > 0) throw new Error("collection.list({ select }) returns plain projection rows and cannot eager-load relationships. Remove include or omit select.");
|
|
606
|
-
|
|
607
|
-
if (orderBy) {
|
|
608
|
-
orderBySql = " ORDER BY ";
|
|
609
|
-
orderBySql += (Array.isArray(orderBy) ? orderBy : [orderBy]).map((item) => {
|
|
610
|
-
const [field, direction = "ASC"] = item.split(" ");
|
|
611
|
-
if (!/^[a-zA-Z0-9_]+$/.test(field)) throw new Error(`Invalid field name for ordering: ${field}`);
|
|
612
|
-
const normalizedDirection = direction.toUpperCase();
|
|
613
|
-
if (normalizedDirection !== "ASC" && normalizedDirection !== "DESC") throw new Error(`Invalid sort direction: ${direction}. Must be ASC or DESC.`);
|
|
614
|
-
return `${toSnakeCase(field)} ${normalizedDirection}`;
|
|
615
|
-
}).join(", ");
|
|
616
|
-
}
|
|
800
|
+
const orderBySql = this.buildOrderBySql(orderBy, fields);
|
|
617
801
|
let limitOffsetSql = "";
|
|
618
802
|
const limitOffsetValues = [];
|
|
619
803
|
let paramIndex = whereValues.length + 1;
|
|
620
|
-
|
|
804
|
+
const boundedLimit = this.applyListBounds(assertQueryBound(limit, "limit"));
|
|
805
|
+
const boundedOffset = assertQueryBound(offset, "offset");
|
|
806
|
+
if (boundedLimit !== void 0) {
|
|
621
807
|
limitOffsetSql += ` LIMIT $${paramIndex++}`;
|
|
622
|
-
limitOffsetValues.push(
|
|
808
|
+
limitOffsetValues.push(boundedLimit);
|
|
623
809
|
}
|
|
624
|
-
if (
|
|
810
|
+
if (boundedOffset !== void 0) {
|
|
625
811
|
limitOffsetSql += ` OFFSET $${paramIndex++}`;
|
|
626
|
-
limitOffsetValues.push(
|
|
812
|
+
limitOffsetValues.push(boundedOffset);
|
|
627
813
|
}
|
|
628
814
|
const sql = `SELECT ${projection ? projection.sql : "*"} FROM ${this.tableName} ${whereSql} ${orderBySql} ${limitOffsetSql}`;
|
|
629
815
|
const params = [...whereValues, ...limitOffsetValues];
|
|
@@ -1289,11 +1475,41 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1289
1475
|
return instances;
|
|
1290
1476
|
}
|
|
1291
1477
|
/**
|
|
1478
|
+
* Resolve the `_smrt_contexts.owner_id` key for collection-level memory
|
|
1479
|
+
* (#2365).
|
|
1480
|
+
*
|
|
1481
|
+
* `_smrt_contexts` has no tenant column, so before this hook a tenant-scoped
|
|
1482
|
+
* collection's learned memory (`remember()`/`recall()`) was filed under one
|
|
1483
|
+
* shared `__collection__` key and leaked across tenants. For a tenant-scoped
|
|
1484
|
+
* item class under an active tenant context the key becomes
|
|
1485
|
+
* `__collection__:<tenantId>`; everything else (tenancy disabled, no active
|
|
1486
|
+
* tenant, system context, non-tenant-scoped class) keeps the shared
|
|
1487
|
+
* `__collection__` key. Tenant-keyed memory is strictly isolated — a tenant
|
|
1488
|
+
* recall does NOT fall back to the shared key, so memory learned outside a
|
|
1489
|
+
* tenant context is invisible inside one (and vice versa).
|
|
1490
|
+
*
|
|
1491
|
+
* Tenant resolution uses the same core-side trust anchors as the generated
|
|
1492
|
+
* REST read scope (#1782): `resolveDispatchTenantScope()` for the active
|
|
1493
|
+
* tenant and the triple tenant-scoped-class check covering `@smrt()` config,
|
|
1494
|
+
* manifest config, and the tenancy-filled `@TenantScoped()` resolver.
|
|
1495
|
+
*/
|
|
1496
|
+
resolveCollectionMemoryOwnerId() {
|
|
1497
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
1498
|
+
if (!(ObjectRegistry.isTenantScoped(itemClassName) || !!ObjectRegistry.getConfig(itemClassName)?.tenantScoped || isTenantScopedClassResolved(itemClassName))) return COLLECTION_MEMORY_OWNER_ID;
|
|
1499
|
+
const scope = resolveDispatchTenantScope();
|
|
1500
|
+
if (!scope.enforced || !scope.tenantId) return COLLECTION_MEMORY_OWNER_ID;
|
|
1501
|
+
return `${COLLECTION_MEMORY_OWNER_ID}:${scope.tenantId}`;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1292
1504
|
* Remember collection-level context
|
|
1293
1505
|
*
|
|
1294
1506
|
* Stores context applicable to all instances of this collection type.
|
|
1295
1507
|
* Use for patterns that apply to the entire collection (e.g., default parsing strategies).
|
|
1296
1508
|
*
|
|
1509
|
+
* Under an active tenant context on a tenant-scoped class, memory is keyed
|
|
1510
|
+
* per tenant and never shared across tenants (#2365) — see
|
|
1511
|
+
* {@link resolveCollectionMemoryOwnerId}.
|
|
1512
|
+
*
|
|
1297
1513
|
* `expiresAt` is stored on the `_smrt_contexts` row as caller-managed metadata.
|
|
1298
1514
|
* {@link recall} and {@link recallAll} do **not** filter on it, so an expired
|
|
1299
1515
|
* entry is still returned; filter or delete expired entries yourself.
|
|
@@ -1333,7 +1549,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1333
1549
|
], {
|
|
1334
1550
|
id,
|
|
1335
1551
|
owner_class: this._itemClass.name,
|
|
1336
|
-
owner_id:
|
|
1552
|
+
owner_id: this.resolveCollectionMemoryOwnerId(),
|
|
1337
1553
|
scope: options.scope,
|
|
1338
1554
|
key: options.key,
|
|
1339
1555
|
value: JSON.stringify(options.value),
|
|
@@ -1367,12 +1583,13 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1367
1583
|
*/
|
|
1368
1584
|
async recall(options) {
|
|
1369
1585
|
if (!this.systemDb) throw new Error("Database not initialized. Call initialize() first.");
|
|
1586
|
+
const ownerId = this.resolveCollectionMemoryOwnerId();
|
|
1370
1587
|
let result;
|
|
1371
1588
|
if (options.minConfidence !== void 0) result = await this.systemDb.single`
|
|
1372
1589
|
SELECT value, confidence
|
|
1373
1590
|
FROM _smrt_contexts
|
|
1374
1591
|
WHERE owner_class = ${this._itemClass.name}
|
|
1375
|
-
AND owner_id = ${
|
|
1592
|
+
AND owner_id = ${ownerId}
|
|
1376
1593
|
AND scope = ${options.scope}
|
|
1377
1594
|
AND key = ${options.key}
|
|
1378
1595
|
AND confidence >= ${options.minConfidence}
|
|
@@ -1383,7 +1600,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1383
1600
|
SELECT value, confidence
|
|
1384
1601
|
FROM _smrt_contexts
|
|
1385
1602
|
WHERE owner_class = ${this._itemClass.name}
|
|
1386
|
-
AND owner_id = ${
|
|
1603
|
+
AND owner_id = ${ownerId}
|
|
1387
1604
|
AND scope = ${options.scope}
|
|
1388
1605
|
AND key = ${options.key}
|
|
1389
1606
|
ORDER BY confidence DESC, version DESC
|
|
@@ -1438,7 +1655,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1438
1655
|
FROM _smrt_contexts
|
|
1439
1656
|
WHERE owner_class = ? AND owner_id = ?
|
|
1440
1657
|
`;
|
|
1441
|
-
const params = [this._itemClass.name,
|
|
1658
|
+
const params = [this._itemClass.name, this.resolveCollectionMemoryOwnerId()];
|
|
1442
1659
|
if (options.scope) if (options.includeDescendants) {
|
|
1443
1660
|
query += ` AND (scope = ? OR scope LIKE ?)`;
|
|
1444
1661
|
params.push(options.scope, `${options.scope}/%`);
|
|
@@ -1483,7 +1700,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1483
1700
|
async forget(options) {
|
|
1484
1701
|
if (!this.systemDb) throw new Error("Database not initialized. Call initialize() first.");
|
|
1485
1702
|
await this.systemDb.query(`DELETE FROM _smrt_contexts
|
|
1486
|
-
WHERE owner_class = ? AND owner_id = ? AND scope = ? AND key = ?`, this._itemClass.name,
|
|
1703
|
+
WHERE owner_class = ? AND owner_id = ? AND scope = ? AND key = ?`, this._itemClass.name, this.resolveCollectionMemoryOwnerId(), options.scope, options.key);
|
|
1487
1704
|
}
|
|
1488
1705
|
/**
|
|
1489
1706
|
* Forget all collection-level context in a scope
|
|
@@ -1507,7 +1724,7 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1507
1724
|
DELETE FROM _smrt_contexts
|
|
1508
1725
|
WHERE owner_class = ? AND owner_id = ?
|
|
1509
1726
|
`;
|
|
1510
|
-
const params = [this._itemClass.name,
|
|
1727
|
+
const params = [this._itemClass.name, this.resolveCollectionMemoryOwnerId()];
|
|
1511
1728
|
if (options.includeDescendants) {
|
|
1512
1729
|
query += ` AND (scope = ? OR scope LIKE ?)`;
|
|
1513
1730
|
params.push(options.scope, `${options.scope}/%`);
|
|
@@ -1524,6 +1741,11 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1524
1741
|
* Generates an embedding for the query text and finds similar objects
|
|
1525
1742
|
* based on cosine similarity of stored embeddings.
|
|
1526
1743
|
*
|
|
1744
|
+
* Under an active tenant context on a tenant-scoped class, candidates are
|
|
1745
|
+
* restricted to the tenant's rows BEFORE top-K ranking (#2365), so results
|
|
1746
|
+
* are never starved by — and similarity ranks never leak — other tenants'
|
|
1747
|
+
* content.
|
|
1748
|
+
*
|
|
1527
1749
|
* @param query - Text to search for
|
|
1528
1750
|
* @param options - Search options
|
|
1529
1751
|
* @param options.field - Specific field to search (defaults to first embedding
|
|
@@ -1656,11 +1878,22 @@ var SmrtCollection = class SmrtCollection extends SmrtClass {
|
|
|
1656
1878
|
fallbackToAI: embeddingConfig.fallbackToAI
|
|
1657
1879
|
}, this.ai).getModelName();
|
|
1658
1880
|
const vector = (ObjectRegistry.getProjectEmbeddingConfig()?.storage || "json") === "native" ? this.systemDb.vector : void 0;
|
|
1881
|
+
const itemClassName = this.getResolvedItemClassName();
|
|
1882
|
+
const tenantPrefilterContext = createInterceptorContext(itemClassName, "list", this.constructor.name);
|
|
1883
|
+
const tenantPrefilter = await GlobalInterceptors.executeBeforeList(itemClassName, { where: {} }, tenantPrefilterContext);
|
|
1884
|
+
let candidateObjectIds;
|
|
1885
|
+
if (tenantPrefilter.where && Object.keys(tenantPrefilter.where).length > 0) {
|
|
1886
|
+
const { sql: candidateWhereSql, values: candidateValues } = buildWhere(this.convertWhereKeys(tenantPrefilter.where));
|
|
1887
|
+
const { rows: candidateRows } = await this.db.query(`SELECT id FROM ${this.tableName} ${candidateWhereSql}`, ...candidateValues);
|
|
1888
|
+
candidateObjectIds = candidateRows.map((row) => String(row.id));
|
|
1889
|
+
if (candidateObjectIds.length === 0) return [];
|
|
1890
|
+
}
|
|
1659
1891
|
const scored = await EmbeddingStorage.searchSimilar(this.systemDb, this._itemClass.name, embedding, {
|
|
1660
1892
|
field: searchField,
|
|
1661
1893
|
model,
|
|
1662
1894
|
limit,
|
|
1663
|
-
minSimilarity
|
|
1895
|
+
minSimilarity,
|
|
1896
|
+
objectIds: candidateObjectIds
|
|
1664
1897
|
}, vector);
|
|
1665
1898
|
if (scored.length === 0) return [];
|
|
1666
1899
|
const objectIds = scored.map((s) => s.objectId);
|