@happyvertical/smrt-core 0.40.70 → 0.42.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 +3 -3
- package/agents/change-feed.md +1 -1
- package/agents/schema-paths.md +561 -32
- package/dist/browser.js +5 -4
- 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 +54 -13
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +8 -1
- package/dist/change-signals.js.map +1 -1
- package/dist/class.d.ts +36 -3
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +69 -14
- 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 +29 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +68 -16
- 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/decorators/index.d.ts +63 -5
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +37 -5
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
- package/dist/dispatch/collections/Dispatches.js +21 -6
- package/dist/dispatch/collections/Dispatches.js.map +1 -1
- package/dist/dispatch/models/Dispatch.d.ts +1 -1
- package/dist/dispatch/models/Dispatch.d.ts.map +1 -1
- package/dist/dispatch/models/Dispatch.js +3 -2
- package/dist/dispatch/models/Dispatch.js.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts +1 -1
- package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -1
- package/dist/dispatch/models/DispatchSubscription.js +2 -1
- package/dist/dispatch/models/DispatchSubscription.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 +31 -13
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/hierarchical.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- 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/learning/memory.d.ts.map +1 -1
- package/dist/learning/memory.js +5 -4
- package/dist/learning/memory.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 +9 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +12 -7
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -1
- package/dist/migrations/integer-width.d.ts +99 -0
- package/dist/migrations/integer-width.d.ts.map +1 -0
- package/dist/migrations/integer-width.js +237 -0
- package/dist/migrations/integer-width.js.map +1 -0
- 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 +383 -0
- package/dist/migrations/minor-units.js.map +1 -0
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +28 -20
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +3 -1
- package/dist/object.d.ts +98 -5
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +138 -30
- package/dist/object.js.map +1 -1
- package/dist/query-bounds.d.ts +5 -1
- package/dist/query-bounds.d.ts.map +1 -1
- package/dist/query-bounds.js +5 -1
- package/dist/query-bounds.js.map +1 -1
- 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 +3 -2
- 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/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/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +3 -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 +14 -1
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +245 -41
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +397 -241
- 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 +2 -3
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -3
- package/dist/schema/live-parity.d.ts +3 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +23 -2
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +13 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -1
- package/dist/schema/system-table-shapes.js +14 -1
- package/dist/schema/system-table-shapes.js.map +1 -1
- package/dist/schema/types.d.ts +17 -10
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -3
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +2 -3
- package/dist/smrt-knowledge.json +9 -9
- 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 +102 -16
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +85 -47
- 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.map +1 -1
- package/dist/utils/safe-integer.d.ts +19 -0
- package/dist/utils/safe-integer.d.ts.map +1 -0
- package/dist/utils/safe-integer.js +31 -0
- package/dist/utils/safe-integer.js.map +1 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -3
- package/dist/utils.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -62
- package/dist/vite-plugin/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/schema/override-system.d.ts +0 -43
- package/dist/schema/override-system.d.ts.map +0 -1
- package/dist/schema/override-system.js +0 -205
- package/dist/schema/override-system.js.map +0 -1
package/dist/object.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { classifyDatabaseError, classifyDialectMessage } from "./db-errors.js";
|
|
2
|
+
import { DatabaseError, ErrorUtils, RuntimeError, SmrtError, TenantIsolationError, ValidationError } from "./errors.js";
|
|
3
|
+
import { toSnakeCase } from "./utils/naming.js";
|
|
4
|
+
import { runCascadeDelete } from "./cascade.js";
|
|
1
5
|
import { broadcastCacheInvalidation, hasCrossProcessCacheInterest, invalidateCollectionCache, resolveDbCacheKey } from "./collection-cache.js";
|
|
2
|
-
import {
|
|
6
|
+
import { isEmbeddedDatabase, withEmbeddedWriteQueue } from "./embedded-write-queue.js";
|
|
7
|
+
import { GlobalInterceptors, createInterceptorContext, resolveGetStringFilter } from "./interceptors.js";
|
|
3
8
|
import { SmrtClass } from "./class.js";
|
|
4
9
|
import { EmbeddingProvider } from "./embeddings/provider.js";
|
|
5
10
|
import { EmbeddingStorage } from "./embeddings/storage.js";
|
|
6
|
-
import { classifyDatabaseError, classifyDialectMessage } from "./db-errors.js";
|
|
7
|
-
import { DatabaseError, ErrorUtils, RuntimeError, SmrtError, TenantIsolationError, ValidationError } from "./errors.js";
|
|
8
|
-
import { toSnakeCase } from "./utils/naming.js";
|
|
9
11
|
import { ContentHasher } from "./embeddings/hash.js";
|
|
10
12
|
import { verifyPersistenceTable } from "./schema/table-verifier.js";
|
|
11
13
|
import { executeToolCall } from "./tools/tool-executor.js";
|
|
12
|
-
import { fieldsFromClass, tableNameFromClass } from "./utils.js";
|
|
14
|
+
import { fieldsFromClass, keysToSnakeCase, tableNameFromClass } from "./utils.js";
|
|
13
15
|
import { ObjectRegistry } from "./registry.js";
|
|
14
16
|
import { createLogger } from "@happyvertical/logger";
|
|
15
17
|
//#region src/object.ts
|
|
@@ -659,9 +661,9 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
659
661
|
const value = this.getPropertyValue(key);
|
|
660
662
|
if (value === void 0) {
|
|
661
663
|
const fieldType = prop && typeof prop === "object" && "type" in prop && prop.type || fieldDef?.type;
|
|
662
|
-
if (
|
|
664
|
+
if ((prop && typeof prop === "object" && "__tenancy" in prop ? prop.__tenancy : void 0)?.isTenantIdField || fieldDef?.__tenancy?.isTenantIdField || fieldDef?._meta?.__tenancy?.isTenantIdField) if (isSTI && fieldDef?.type === "meta") metaData[key] = null;
|
|
663
665
|
else data[key] = null;
|
|
664
|
-
else if (isSTI && fieldDef?.type === "meta") metaData[key] = "";
|
|
666
|
+
else if (fieldType === "text") if (isSTI && fieldDef?.type === "meta") metaData[key] = "";
|
|
665
667
|
else data[key] = "";
|
|
666
668
|
else if (fieldType === "json") {
|
|
667
669
|
const defaultValue = fieldDef?.default ?? null;
|
|
@@ -801,15 +803,21 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
801
803
|
/**
|
|
802
804
|
* Gets or generates a unique ID for this object
|
|
803
805
|
*
|
|
806
|
+
* The natural-key lookup runs through the `beforeGet` interceptor pipeline
|
|
807
|
+
* (#2365), so under an active tenant context this can only adopt the id of
|
|
808
|
+
* a row visible to that tenant — never another tenant's same-slug row,
|
|
809
|
+
* which would both disclose the foreign id and steer a subsequent `save()`
|
|
810
|
+
* onto the foreign row.
|
|
811
|
+
*
|
|
804
812
|
* @returns Promise resolving to the object's ID
|
|
805
813
|
*/
|
|
806
814
|
async getId() {
|
|
807
815
|
if (this.slug) {
|
|
808
816
|
await this.verifyStorageReady();
|
|
809
|
-
const saved = await this.db.get(this.tableName, {
|
|
817
|
+
const saved = await this.db.get(this.tableName, await this.interceptGetFilter({
|
|
810
818
|
slug: this.slug,
|
|
811
819
|
context: this.context
|
|
812
|
-
});
|
|
820
|
+
}));
|
|
813
821
|
if (saved) this.id = saved.id;
|
|
814
822
|
}
|
|
815
823
|
if (!this.id) this.id = crypto.randomUUID();
|
|
@@ -850,22 +858,56 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
850
858
|
/**
|
|
851
859
|
* Gets the ID of this object if it's already saved in the database
|
|
852
860
|
*
|
|
861
|
+
* Both lookups run through the `beforeGet` interceptor pipeline (#2365), so
|
|
862
|
+
* under an active tenant context only rows visible to that tenant count as
|
|
863
|
+
* "saved".
|
|
864
|
+
*
|
|
853
865
|
* @returns Promise resolving to the saved ID or null if not saved
|
|
854
866
|
*/
|
|
855
867
|
async getSavedId() {
|
|
856
868
|
if (!this.id && !this.slug) return null;
|
|
857
869
|
await this.verifyStorageReady();
|
|
858
870
|
if (this.id) {
|
|
859
|
-
const byId = await this.db.get(this.tableName, { id: this.id });
|
|
871
|
+
const byId = await this.db.get(this.tableName, await this.interceptGetFilter({ id: this.id }));
|
|
860
872
|
if (byId) return byId.id;
|
|
861
873
|
}
|
|
862
874
|
if (this.slug) {
|
|
863
|
-
const bySlug = await this.db.get(this.tableName, { slug: this.slug });
|
|
875
|
+
const bySlug = await this.db.get(this.tableName, await this.interceptGetFilter({ slug: this.slug }));
|
|
864
876
|
if (bySlug) return bySlug.id;
|
|
865
877
|
}
|
|
866
878
|
return null;
|
|
867
879
|
}
|
|
868
880
|
/**
|
|
881
|
+
* Run the registered `beforeGet` interceptors over a direct hydration filter
|
|
882
|
+
* and return it in database column form (#2365).
|
|
883
|
+
*
|
|
884
|
+
* `loadFromId()`, `loadFromSlug()`, `getSavedId()` and `getId()` query
|
|
885
|
+
* `this.db.get` directly instead of going through `SmrtCollection.get()`, so
|
|
886
|
+
* before this hook they bypassed every read interceptor: under an active tenant context,
|
|
887
|
+
* `new Model({ slug }).initialize()` happily hydrated another tenant's row
|
|
888
|
+
* even though every collection read was filtered. Routing the filter through
|
|
889
|
+
* `GlobalInterceptors.executeBeforeGet` gives hydration the same read
|
|
890
|
+
* predicate as collection reads — the tenancy interceptor adds its tenant
|
|
891
|
+
* filter (or throws `TenantContextError` for `mode: 'required'` classes
|
|
892
|
+
* outside a tenant context), and the existing bypasses (`withSystemContext`,
|
|
893
|
+
* super-admin bypass) keep working because they short-circuit inside the
|
|
894
|
+
* interceptor itself.
|
|
895
|
+
*
|
|
896
|
+
* Interceptors speak field names (`tenantId`); `this.db.get` speaks column
|
|
897
|
+
* names, so the merged filter is converted to snake_case before use. The
|
|
898
|
+
* base filters (`id`, `slug`, `context`) are already column-shaped and pass
|
|
899
|
+
* through unchanged.
|
|
900
|
+
*
|
|
901
|
+
* @param filter - The column-shaped hydration filter to intercept
|
|
902
|
+
* @returns The intercepted filter with all keys in column form
|
|
903
|
+
*/
|
|
904
|
+
async interceptGetFilter(filter) {
|
|
905
|
+
const className = this.getResolvedClassName();
|
|
906
|
+
const interceptorContext = createInterceptorContext(className, "get");
|
|
907
|
+
const intercepted = await GlobalInterceptors.executeBeforeGet(className, filter, interceptorContext);
|
|
908
|
+
return keysToSnakeCase(typeof intercepted === "string" ? resolveGetStringFilter(intercepted) : intercepted);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
869
911
|
* Checks if this object is already saved in the database
|
|
870
912
|
*
|
|
871
913
|
* @returns Promise resolving to true if saved, false otherwise
|
|
@@ -1025,7 +1067,8 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1025
1067
|
const conflictColumns = ObjectRegistry.getConflictColumns(className);
|
|
1026
1068
|
const writePlan = await this.planPersistenceWrite(className, tableStrategy, data, conflictColumns);
|
|
1027
1069
|
const upsertConflictColumns = this._persisted && data.id ? ["id"] : conflictColumns;
|
|
1028
|
-
|
|
1070
|
+
const serializeEmbeddedWrite = writePlan.type !== "updateById" && !(this._insertOnly && !this._persisted) && isEmbeddedDatabase(this.db) && upsertConflictColumns.some((column) => data[column] == null);
|
|
1071
|
+
await withEmbeddedWriteQueue(this.db, serializeEmbeddedWrite, () => ErrorUtils.withRetry(async () => {
|
|
1029
1072
|
try {
|
|
1030
1073
|
if (writePlan.type === "updateById") {
|
|
1031
1074
|
const { id: _id, ...updateData } = data;
|
|
@@ -1049,7 +1092,7 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1049
1092
|
}
|
|
1050
1093
|
throw error;
|
|
1051
1094
|
}
|
|
1052
|
-
}, 3, 500);
|
|
1095
|
+
}, 3, 500));
|
|
1053
1096
|
this._persisted = true;
|
|
1054
1097
|
this.invalidateCollectionReadCache();
|
|
1055
1098
|
await GlobalInterceptors.executeAfterSave(this, interceptorContext);
|
|
@@ -1246,8 +1289,10 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1246
1289
|
/**
|
|
1247
1290
|
* Hydrates this object from the database using its `id` property.
|
|
1248
1291
|
*
|
|
1249
|
-
* Queries the database for a row matching `{ id: this._id }`
|
|
1250
|
-
*
|
|
1292
|
+
* Queries the database for a row matching `{ id: this._id }` — after running
|
|
1293
|
+
* the filter through the `beforeGet` interceptor pipeline (#2365), so tenant
|
|
1294
|
+
* scoping applies to hydration exactly as it does to collection reads — and
|
|
1295
|
+
* calls `loadDataFromDb()` if found. Transient failures are retried up to 4 times
|
|
1251
1296
|
* total — the initial try plus 3 retries — sleeping 250, 500 and 1000 ms
|
|
1252
1297
|
* between them; deterministic failures are not. A malformed identifier
|
|
1253
1298
|
* (PostgreSQL `22P02 invalid_text_representation` on a `uuid` column) is a
|
|
@@ -1271,12 +1316,13 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1271
1316
|
* ```
|
|
1272
1317
|
*/
|
|
1273
1318
|
async loadFromId() {
|
|
1319
|
+
if (!this._id) throw ValidationError.requiredField("id", this.constructor.name);
|
|
1320
|
+
const filter = await this.interceptGetFilter({ id: this._id });
|
|
1274
1321
|
try {
|
|
1275
|
-
if (!this._id) throw ValidationError.requiredField("id", this.constructor.name);
|
|
1276
1322
|
await this.verifyStorageReady();
|
|
1277
1323
|
await ErrorUtils.withRetry(async () => {
|
|
1278
1324
|
try {
|
|
1279
|
-
const existing = await this.db.get(this.tableName,
|
|
1325
|
+
const existing = await this.db.get(this.tableName, filter);
|
|
1280
1326
|
if (existing) await this.loadDataFromDb(existing);
|
|
1281
1327
|
} catch (error) {
|
|
1282
1328
|
throw DatabaseError.queryFailed(`get(${this.tableName}, { id: ${this._id} })`, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -1308,10 +1354,10 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1308
1354
|
*/
|
|
1309
1355
|
async loadFromSlug() {
|
|
1310
1356
|
await this.verifyStorageReady();
|
|
1311
|
-
const existing = await this.db.get(this.tableName, {
|
|
1357
|
+
const existing = await this.db.get(this.tableName, await this.interceptGetFilter({
|
|
1312
1358
|
slug: this._slug,
|
|
1313
1359
|
context: this._context || ""
|
|
1314
|
-
});
|
|
1360
|
+
}));
|
|
1315
1361
|
if (existing) await this.loadDataFromDb(existing);
|
|
1316
1362
|
}
|
|
1317
1363
|
/**
|
|
@@ -1510,34 +1556,78 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1510
1556
|
} else if (typeof hook === "function") await hook(this);
|
|
1511
1557
|
}
|
|
1512
1558
|
/**
|
|
1513
|
-
* Deletes this object from the database
|
|
1559
|
+
* Deletes this object from the database, cascading to the rows that
|
|
1560
|
+
* reference it.
|
|
1514
1561
|
*
|
|
1515
1562
|
* Runs the full lifecycle in order:
|
|
1516
1563
|
* 1. `beforeDelete` interceptors (e.g. tenant validation)
|
|
1517
1564
|
* 2. `beforeDelete` lifecycle hook (defined in `@smrt({ hooks })`)
|
|
1518
|
-
* 3.
|
|
1565
|
+
* 3. Referential cleanup and the row deletion (one transaction when there
|
|
1566
|
+
* is anything to clean up — see below)
|
|
1519
1567
|
* 4. `afterDelete` lifecycle hook
|
|
1520
1568
|
* 5. `afterDelete` interceptors
|
|
1521
1569
|
*
|
|
1570
|
+
* ## Referential cleanup (#2371)
|
|
1571
|
+
*
|
|
1572
|
+
* SMRT emits no DB-level `FOREIGN KEY` constraints, so step 3 enforces
|
|
1573
|
+
* referential integrity in the application layer instead:
|
|
1574
|
+
*
|
|
1575
|
+
* - `_smrt_embeddings` and `_smrt_contexts` rows owned by this object are
|
|
1576
|
+
* removed, so a deleted object can no longer win a `semanticSearch` slot
|
|
1577
|
+
* or resurrect stale `recall()` values.
|
|
1578
|
+
* - Polymorphic association rows whose `(metaType, metaId)` points here are
|
|
1579
|
+
* removed.
|
|
1580
|
+
* - `@foreignKey` / `@crossPackageRef` columns pointing at this class are
|
|
1581
|
+
* resolved according to their declared `onDelete` — `'CASCADE'`,
|
|
1582
|
+
* `'SET NULL'` or `'RESTRICT'`. Without a declaration, a column that is
|
|
1583
|
+
* part of the referencing class's `conflictColumns` (junction and
|
|
1584
|
+
* association rows) defaults to `CASCADE`; every other column is left
|
|
1585
|
+
* untouched, as before.
|
|
1586
|
+
*
|
|
1587
|
+
* Cascaded rows are removed with set-based statements: their own hooks and
|
|
1588
|
+
* interceptors do **not** run and no change-feed tombstone is written for
|
|
1589
|
+
* them, exactly as a DB-level `ON DELETE CASCADE` behaves. Only this object
|
|
1590
|
+
* runs the lifecycle above.
|
|
1591
|
+
*
|
|
1592
|
+
* When anything references this class, step 3 runs inside a single
|
|
1593
|
+
* transaction when the adapter supports one, so a failed cascade cannot
|
|
1594
|
+
* leave the object deleted with its junction rows intact. When nothing
|
|
1595
|
+
* references it, there is nothing to keep consistent and the transaction
|
|
1596
|
+
* is skipped so an ordinary delete costs no extra round trip — but that
|
|
1597
|
+
* skip requires no registered polymorphic association class anywhere in
|
|
1598
|
+
* the process, not just no typed reference to this class (see
|
|
1599
|
+
* `cascade.ts`'s module doc: a `metaType` column can point at any class at
|
|
1600
|
+
* runtime, so a polymorphic association class is always plausibly
|
|
1601
|
+
* relevant).
|
|
1602
|
+
*
|
|
1522
1603
|
* Prefer `collection.delete(id)` from application code — it loads the
|
|
1523
1604
|
* object first (returning `false` when not found) before calling this method.
|
|
1524
1605
|
*
|
|
1525
1606
|
* @returns Promise that resolves when deletion is complete
|
|
1607
|
+
* @throws {DatabaseError} When a referencing column declares
|
|
1608
|
+
* `onDelete: 'RESTRICT'` and rows still point at this object
|
|
1526
1609
|
*
|
|
1527
1610
|
* @example
|
|
1528
1611
|
* ```typescript
|
|
1529
1612
|
* const product = await products.get('product-uuid');
|
|
1530
1613
|
* if (product) await product.delete();
|
|
1531
1614
|
* ```
|
|
1615
|
+
*
|
|
1616
|
+
* @see {@link foreignKey} for declaring `onDelete` on the referencing side
|
|
1532
1617
|
*/
|
|
1533
1618
|
async delete() {
|
|
1534
1619
|
const interceptorContext = createInterceptorContext(this.constructor.name, "delete");
|
|
1535
1620
|
await GlobalInterceptors.executeBeforeDelete(this, interceptorContext);
|
|
1536
1621
|
await this.runHook("beforeDelete");
|
|
1537
1622
|
await this.verifyStorageReady();
|
|
1538
|
-
await this.db
|
|
1623
|
+
const { affectedTables, affectedTableClasses } = await runCascadeDelete(this.db, ObjectRegistry, {
|
|
1624
|
+
className: this.getResolvedQualifiedName(),
|
|
1625
|
+
tableName: this.tableName,
|
|
1626
|
+
id: this.id
|
|
1627
|
+
}, (db) => db.delete(this.tableName, { id: this.id }).then(() => void 0));
|
|
1539
1628
|
this._persisted = false;
|
|
1540
1629
|
this.invalidateCollectionReadCache();
|
|
1630
|
+
for (const table of affectedTables) this.invalidateCollectionReadCache(table, affectedTableClasses.get(table));
|
|
1541
1631
|
await this.runHook("afterDelete");
|
|
1542
1632
|
await GlobalInterceptors.executeAfterDelete(this, interceptorContext);
|
|
1543
1633
|
}
|
|
@@ -1551,15 +1641,21 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1551
1641
|
* broadcast to peer replicas over the database adapter's notification
|
|
1552
1642
|
* capability, fire-and-forget. Cache maintenance must never fail the
|
|
1553
1643
|
* write that triggered it.
|
|
1644
|
+
*
|
|
1645
|
+
* @param tableName - Table to invalidate; defaults to this object's own.
|
|
1646
|
+
* @param ownerQualifiedClassName - Qualified name of the class that owns
|
|
1647
|
+
* `tableName`, when it differs from this object's own class (a
|
|
1648
|
+
* cascade-affected table, #2371) — lets the broadcast decision read
|
|
1649
|
+
* *that* class's `@smrt({ cache })` config instead of only this one's.
|
|
1554
1650
|
*/
|
|
1555
|
-
invalidateCollectionReadCache() {
|
|
1651
|
+
invalidateCollectionReadCache(tableName = this.tableName, ownerQualifiedClassName) {
|
|
1556
1652
|
try {
|
|
1557
1653
|
const dbKey = resolveDbCacheKey(this.db);
|
|
1558
|
-
invalidateCollectionCache(dbKey,
|
|
1559
|
-
if (this.shouldBroadcastCacheInvalidation(dbKey)) broadcastCacheInvalidation(this.db,
|
|
1654
|
+
invalidateCollectionCache(dbKey, tableName);
|
|
1655
|
+
if (this.shouldBroadcastCacheInvalidation(dbKey, tableName, ownerQualifiedClassName)) broadcastCacheInvalidation(this.db, tableName);
|
|
1560
1656
|
} catch (error) {
|
|
1561
1657
|
logger.warn("Failed to invalidate collection read cache after write", {
|
|
1562
|
-
table:
|
|
1658
|
+
table: tableName,
|
|
1563
1659
|
error: error instanceof Error ? error.message : error
|
|
1564
1660
|
});
|
|
1565
1661
|
}
|
|
@@ -1570,14 +1666,26 @@ var SmrtObject = class SmrtObject extends SmrtClass {
|
|
|
1570
1666
|
*
|
|
1571
1667
|
* 1. A per-call `crossProcess` cached read in this process registered
|
|
1572
1668
|
* interest in the table — broadcast even without model-level config.
|
|
1573
|
-
* 2.
|
|
1669
|
+
* 2. The owning class's resolved `@smrt({ cache })` config sets
|
|
1670
|
+
* `crossProcess`. For this object's own table that class is `this`;
|
|
1671
|
+
* for a cascade-affected table (#2371) it is whatever class the
|
|
1672
|
+
* cascade plan attributed the table to, passed in as
|
|
1673
|
+
* `ownerQualifiedClassName` — a different class's table needs *that*
|
|
1674
|
+
* class's config checked, not this one's.
|
|
1574
1675
|
* 3. Any other STI hierarchy member sharing the table resolves to a
|
|
1575
1676
|
* `crossProcess` config — a child that opted out with `cache: false`
|
|
1576
1677
|
* still mutates the shared table its base/siblings are caching.
|
|
1678
|
+
*
|
|
1679
|
+
* @param ownerQualifiedClassName - Qualified class name to check rules 2
|
|
1680
|
+
* and 3 against. Defaults to this object's own resolved qualified name
|
|
1681
|
+
* when `tableName` is this object's own table; when omitted for a
|
|
1682
|
+
* foreign table (the owning class could not be resolved), rules 2/3 are
|
|
1683
|
+
* skipped and only rule 1 applies.
|
|
1577
1684
|
*/
|
|
1578
|
-
shouldBroadcastCacheInvalidation(dbKey) {
|
|
1579
|
-
if (hasCrossProcessCacheInterest(dbKey,
|
|
1580
|
-
const qualifiedName = this.getResolvedQualifiedName();
|
|
1685
|
+
shouldBroadcastCacheInvalidation(dbKey, tableName = this.tableName, ownerQualifiedClassName) {
|
|
1686
|
+
if (hasCrossProcessCacheInterest(dbKey, tableName)) return true;
|
|
1687
|
+
const qualifiedName = ownerQualifiedClassName ?? (tableName === this.tableName ? this.getResolvedQualifiedName() : void 0);
|
|
1688
|
+
if (!qualifiedName) return false;
|
|
1581
1689
|
if (ObjectRegistry.resolveCollectionCacheConfig(qualifiedName)?.crossProcess) return true;
|
|
1582
1690
|
for (const member of getSTIHierarchyMembers(qualifiedName)) {
|
|
1583
1691
|
if (member === qualifiedName) continue;
|