@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/browser.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './class';
|
|
|
18
18
|
export * from './collection';
|
|
19
19
|
export type { AiUsageConfig, GlobalSignalConfig, MetricsConfig, PubSubConfig, } from './config';
|
|
20
20
|
export { config } from './config';
|
|
21
|
+
export { classifyDatabaseError, classifyDialectMessage, type DatabaseErrorClassification, type DatabaseErrorKind, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isTransientDatabaseError, isUniqueViolationError, } from './db-errors';
|
|
21
22
|
export * from './errors';
|
|
22
23
|
export { type HierarchyView, SmrtHierarchical } from './hierarchical';
|
|
23
24
|
export { type JunctionAttachOptions, type JunctionFilterOptions, SmrtJunction, } from './junction';
|
package/dist/browser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAOlC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,KAAK,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,cAAc,UAAU,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,KAAK,iCAAiC,GACvC,MAAM,2BAA2B,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAElD,cAAc,iBAAiB,CAAC;AAEhC,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,cAAc,eAAe,CAAC"}
|
package/dist/browser.js
CHANGED
|
@@ -3,12 +3,13 @@ import { DEFAULT_AI_COST_RATES, estimateAiUsageCost } from "./adapters/cost-rate
|
|
|
3
3
|
import { MetricsAdapter } from "./adapters/metrics.js";
|
|
4
4
|
import { PubSubAdapter } from "./adapters/pubsub.js";
|
|
5
5
|
import "./adapters/index.js";
|
|
6
|
+
import { classifyDatabaseError, classifyDialectMessage, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isTransientDatabaseError, isUniqueViolationError } from "./db-errors.js";
|
|
7
|
+
import { AIError, ConfigurationError, DatabaseError, ErrorUtils, FilesystemError, NetworkError, RuntimeError, SmrtError, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils } from "./errors.js";
|
|
6
8
|
import { config } from "./config.js";
|
|
7
9
|
import { SignalSanitizer } from "./signals/sanitizer.js";
|
|
8
10
|
import { SignalBus } from "./signals/bus.js";
|
|
9
|
-
import { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists } from "./system/compatibility.js";
|
|
11
|
+
import { assertPostgresSystemTimestampsCurrent, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists } from "./system/compatibility.js";
|
|
10
12
|
import { SmrtClass } from "./class.js";
|
|
11
|
-
import { AIError, ConfigurationError, DatabaseError, ErrorUtils, FilesystemError, NetworkError, RuntimeError, SmrtError, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils } from "./errors.js";
|
|
12
13
|
import { convertTypeToJsonSchema, generateToolFromMethod, generateToolManifest, shouldIncludeMethod } from "./tools/tool-generator.js";
|
|
13
14
|
import { executeToolCall, executeToolCalls, formatToolResults, validateToolCall } from "./tools/tool-executor.js";
|
|
14
15
|
import { SmrtObject } from "./object.js";
|
|
@@ -19,6 +20,7 @@ import { SmrtHierarchical } from "./hierarchical.js";
|
|
|
19
20
|
import { SmrtJunction } from "./junction.js";
|
|
20
21
|
import { SmrtPolymorphicAssociation } from "./polymorphic-association.js";
|
|
21
22
|
import "./signals/index.js";
|
|
23
|
+
import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./system/retention.js";
|
|
22
24
|
import "./system/index.js";
|
|
23
25
|
import "./tools/index.js";
|
|
24
|
-
export { AIError, AiUsageCollector, AiUsagePersistenceHandler, ConfigurationError, DEFAULT_AI_COST_RATES, DatabaseError, ErrorUtils, FilesystemError, MetricsAdapter, NetworkError, ObjectRegistry, PubSubAdapter, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, assertPostgresSystemTimestampsCurrent, config, convertTypeToJsonSchema, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, estimateAiUsageCost, executeToolCall, executeToolCalls, formatToolResults, generateToolFromMethod, generateToolManifest, getDatabaseEngine, isSmrtCollectionExtendsName, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, shouldIncludeMethod, smrt, smrt as smrtRegistry, tableExists, validateToolCall };
|
|
26
|
+
export { AIError, AiUsageCollector, AiUsagePersistenceHandler, ConfigurationError, DEFAULT_AI_COST_RATES, DEFAULT_RETENTION_POLICY, DatabaseError, ErrorUtils, FilesystemError, MetricsAdapter, NetworkError, ObjectRegistry, PubSubAdapter, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, assertPostgresSystemTimestampsCurrent, classifyDatabaseError, classifyDialectMessage, clearRetentionTasks, config, convertTypeToJsonSchema, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, estimateAiUsageCost, executeToolCall, executeToolCalls, formatToolResults, generateToolFromMethod, generateToolManifest, getDatabaseEngine, getRetentionTasks, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isSmrtCollectionExtendsName, isTransientDatabaseError, isUniqueViolationError, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, shouldIncludeMethod, smrt, smrt as smrtRegistry, tableExists, unregisterRetentionTask, validateToolCall };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
|
+
import { ObjectRegistry } from './registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Referential action applied to rows pointing at a deleted object.
|
|
5
|
+
*
|
|
6
|
+
* Same vocabulary as SQL's `ON DELETE`, enforced by the framework instead of
|
|
7
|
+
* the engine. `NO ACTION` means "leave the rows alone" — SMRT emits no
|
|
8
|
+
* constraint, so nothing raises.
|
|
9
|
+
*/
|
|
10
|
+
export type OnDeleteAction = 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION';
|
|
11
|
+
/** One reference that must be resolved before the target row can be removed. */
|
|
12
|
+
export interface CascadeReference {
|
|
13
|
+
/** Registered name of the class holding the referencing column. */
|
|
14
|
+
className: string;
|
|
15
|
+
/** Table holding the referencing rows. */
|
|
16
|
+
tableName: string;
|
|
17
|
+
/** Field name (camelCase) on the referencing class. */
|
|
18
|
+
fieldName: string;
|
|
19
|
+
/** Column (snake_case) holding the reference. */
|
|
20
|
+
column: string;
|
|
21
|
+
/** Resolved action, never `NO ACTION` (those are dropped from the plan). */
|
|
22
|
+
action: Exclude<OnDeleteAction, 'NO ACTION'>;
|
|
23
|
+
/** `true` when the action was declared rather than derived from the key. */
|
|
24
|
+
declared: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** A polymorphic association table that can point at any class. */
|
|
27
|
+
export interface CascadePolymorphicReference {
|
|
28
|
+
/** Registered name of the association class. */
|
|
29
|
+
className: string;
|
|
30
|
+
/** Table holding the association rows. */
|
|
31
|
+
tableName: string;
|
|
32
|
+
}
|
|
33
|
+
/** Everything that must happen before rows of one class can be deleted. */
|
|
34
|
+
export interface CascadePlan {
|
|
35
|
+
/** Typed `@foreignKey` / `@crossPackageRef` references, action-resolved. */
|
|
36
|
+
references: CascadeReference[];
|
|
37
|
+
/** Polymorphic association tables that may point at this class. */
|
|
38
|
+
polymorphic: CascadePolymorphicReference[];
|
|
39
|
+
/** `meta_type` values that identify this class in an association row. */
|
|
40
|
+
metaTypes: string[];
|
|
41
|
+
/** `true` when nothing references this class and nothing has to be visited. */
|
|
42
|
+
isEmpty: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read-only slice of `ObjectRegistry` this module needs.
|
|
46
|
+
*
|
|
47
|
+
* Taking it as a parameter — rather than importing the registry as a value —
|
|
48
|
+
* keeps `cascade.ts` out of the `registry → object → cascade` import cycle,
|
|
49
|
+
* and lets tests drive the planner from a hand-built registry.
|
|
50
|
+
*/
|
|
51
|
+
export type CascadeRegistryView = Pick<typeof ObjectRegistry, 'getRelationshipMap' | 'getFields' | 'getConflictColumns' | 'getTableName' | 'getSelfReferableNames' | 'getClass' | 'getSTIBase' | 'getDescendants'>;
|
|
52
|
+
/**
|
|
53
|
+
* Normalize a declared `onDelete` value.
|
|
54
|
+
*
|
|
55
|
+
* Accepts any casing and both `SET NULL` and `SET_NULL`, matching what the
|
|
56
|
+
* schema generator has always carried in the manifest. Returns `undefined` for
|
|
57
|
+
* an unset or unrecognized value so the caller can fall back to the default.
|
|
58
|
+
*/
|
|
59
|
+
export declare function normalizeOnDelete(value: unknown): OnDeleteAction | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Build the cascade plan for one class.
|
|
62
|
+
*
|
|
63
|
+
* The plan is derived entirely from registry metadata, so it is valid for any
|
|
64
|
+
* database the class is used against. It is rebuilt per delete rather than
|
|
65
|
+
* cached: registration is incremental (manifests load lazily, tests register
|
|
66
|
+
* classes between cases) and a stale plan would silently skip a table.
|
|
67
|
+
*
|
|
68
|
+
* @param registry - Registry view (`ObjectRegistry` satisfies it)
|
|
69
|
+
* @param className - Class whose incoming references should be resolved
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildCascadePlan(registry: CascadeRegistryView, className: string): CascadePlan;
|
|
72
|
+
/** Outcome of a cascade run, returned so the caller can invalidate caches. */
|
|
73
|
+
export interface CascadeResult {
|
|
74
|
+
/** Tables whose rows were removed or nulled, excluding the target's own. */
|
|
75
|
+
affectedTables: Set<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Qualified class name that owns each entry in {@link affectedTables},
|
|
78
|
+
* where resolvable — lets the caller check *that* class's own
|
|
79
|
+
* cross-process cache config rather than only its own.
|
|
80
|
+
*/
|
|
81
|
+
affectedTableClasses: Map<string, string>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve every reference to `ids` of `className` and clean their framework
|
|
85
|
+
* side rows, then hand control back so the caller can delete the rows.
|
|
86
|
+
*
|
|
87
|
+
* Callers are expected to run this on a transaction-bound `db` — see
|
|
88
|
+
* {@link runCascadeDelete}, which owns that decision.
|
|
89
|
+
*/
|
|
90
|
+
export declare function cascadeReferencesTo(db: DatabaseInterface, registry: CascadeRegistryView, target: {
|
|
91
|
+
className: string;
|
|
92
|
+
tableName: string;
|
|
93
|
+
ids: string[];
|
|
94
|
+
}): Promise<CascadeResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Run the cascade and the target row's own deletion atomically.
|
|
97
|
+
*
|
|
98
|
+
* When anything references the class and the adapter exposes `transaction()`,
|
|
99
|
+
* the whole sequence runs inside one — including the caller's `deleteSelf`
|
|
100
|
+
* statement, so a failure part-way through cannot leave the object deleted with
|
|
101
|
+
* its junction rows intact (or vice versa). Adapters without transaction
|
|
102
|
+
* support run the same statements sequentially; this is the documented
|
|
103
|
+
* degradation, not a silent one.
|
|
104
|
+
*
|
|
105
|
+
* When nothing references the class there is nothing to keep consistent, and
|
|
106
|
+
* the transaction is skipped — see the comment on that branch.
|
|
107
|
+
*
|
|
108
|
+
* @param db - Database the object is bound to
|
|
109
|
+
* @param registry - Registry view used to build cascade plans
|
|
110
|
+
* @param target - Class, table and id of the object being deleted
|
|
111
|
+
* @param deleteSelf - Issues the target row's own `DELETE`, on the tx-bound db
|
|
112
|
+
* @returns Tables affected by the cascade, for read-cache invalidation
|
|
113
|
+
*/
|
|
114
|
+
export declare function runCascadeDelete(db: DatabaseInterface, registry: CascadeRegistryView, target: {
|
|
115
|
+
className: string;
|
|
116
|
+
tableName: string;
|
|
117
|
+
/** An unsaved object has none; the cascade is then a no-op. */
|
|
118
|
+
id: string | null | undefined;
|
|
119
|
+
}, deleteSelf: (db: DatabaseInterface) => Promise<void>): Promise<CascadeResult>;
|
|
120
|
+
//# sourceMappingURL=cascade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAK5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAMpD;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;AAsB/E,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,mEAAmE;AACnE,MAAM,WAAW,2BAA2B;IAC1C,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,mEAAmE;IACnE,WAAW,EAAE,2BAA2B,EAAE,CAAC;IAC3C,yEAAyE;IACzE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,+EAA+E;IAC/E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,OAAO,cAAc,EACnB,oBAAoB,GACpB,WAAW,GACX,oBAAoB,GACpB,cAAc,GACd,uBAAuB,GACvB,UAAU,GACV,YAAY,GACZ,gBAAgB,CACnB,CAAC;AASF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAM5E;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAC7B,SAAS,EAAE,MAAM,GAChB,WAAW,CAwHb;AA8UD,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B;;;;OAIG;IACH,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,GAC9D,OAAO,CAAC,aAAa,CAAC,CAwBxB;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE;IACN,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC/B,EACD,UAAU,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GACnD,OAAO,CAAC,aAAa,CAAC,CAgDxB"}
|
package/dist/cascade.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { classifyDatabaseError } from "./db-errors.js";
|
|
2
|
+
import { ConfigurationError, DatabaseError } from "./errors.js";
|
|
3
|
+
import { chunkArray } from "./utils/chunk.js";
|
|
4
|
+
import { toSnakeCase } from "./utils/naming.js";
|
|
5
|
+
import { createLogger } from "@happyvertical/logger";
|
|
6
|
+
//#region src/cascade.ts
|
|
7
|
+
/**
|
|
8
|
+
* App-side referential integrity for `SmrtObject.delete()` (#2371).
|
|
9
|
+
*
|
|
10
|
+
* SMRT emits **no** DB-level `FOREIGN KEY` constraints on any engine, so the
|
|
11
|
+
* database will never clean up after a deleted row. Before this module,
|
|
12
|
+
* `delete()` removed the object's own row and nothing else: junction rows,
|
|
13
|
+
* polymorphic association rows, `_smrt_embeddings` and `_smrt_contexts` entries
|
|
14
|
+
* were all left pointing at an id that no longer resolved, and
|
|
15
|
+
* `@foreignKey(..., { onDelete })` was metadata nobody read.
|
|
16
|
+
*
|
|
17
|
+
* This module implements those semantics in the application layer. It mirrors
|
|
18
|
+
* what a DB-level constraint would do, deliberately including the parts that
|
|
19
|
+
* make it *unlike* a model-layer delete:
|
|
20
|
+
*
|
|
21
|
+
* - Cascaded rows are removed with set-based statements. Their `beforeDelete` /
|
|
22
|
+
* `afterDelete` hooks and interceptors do **not** run, and no change-feed
|
|
23
|
+
* tombstone is written for them — exactly as `ON DELETE CASCADE` behaves.
|
|
24
|
+
* Only the object `delete()` was called on runs the full lifecycle.
|
|
25
|
+
* - When anything needs cascading, everything runs inside a single
|
|
26
|
+
* transaction when the adapter exposes one, so a partial cascade cannot
|
|
27
|
+
* survive a failure. A class with no typed references AND no registered
|
|
28
|
+
* polymorphic association class anywhere in the process skips the
|
|
29
|
+
* transaction — see {@link runCascadeDelete}. A `metaType` column can point
|
|
30
|
+
* at any class at runtime, so a polymorphic association class is always
|
|
31
|
+
* plausibly relevant; in an app with even one registered, this fast path is
|
|
32
|
+
* rare, not the common case.
|
|
33
|
+
*
|
|
34
|
+
* ## Which references are followed
|
|
35
|
+
*
|
|
36
|
+
* A reference is any `@foreignKey` / `@crossPackageRef` field on a registered
|
|
37
|
+
* class whose target resolves to the deleted object's class (or one of its
|
|
38
|
+
* registered STI ancestors), plus the polymorphic `(metaType, metaId)` pair on
|
|
39
|
+
* {@link SmrtPolymorphicAssociation} subclasses.
|
|
40
|
+
*
|
|
41
|
+
* The action comes from the field's declared `onDelete`. When it is not
|
|
42
|
+
* declared, the default is derived from the referencing class's natural key:
|
|
43
|
+
*
|
|
44
|
+
* | Reference | Default when `onDelete` is absent |
|
|
45
|
+
* |---|---|
|
|
46
|
+
* | Column is part of the referencing class's `conflictColumns`, and is not a `@tenantId()` field | `CASCADE` |
|
|
47
|
+
* | Polymorphic `(metaType, metaId)` association row | `CASCADE` |
|
|
48
|
+
* | Anything else, including every `@tenantId()` field | `NO ACTION` (legacy behaviour — the row is left alone) |
|
|
49
|
+
*
|
|
50
|
+
* The natural-key rule is what makes junction rows work without any
|
|
51
|
+
* per-package annotation: a junction declares
|
|
52
|
+
* `@smrt({ conflictColumns: ['content_id', 'asset_id', 'relationship'] })`, so
|
|
53
|
+
* `content_id` identifies the row and the row cannot outlive the content it
|
|
54
|
+
* links. An ordinary child (`Order.customerId`) is keyed by `(slug, context)`,
|
|
55
|
+
* so it keeps today's behaviour unless it opts in with
|
|
56
|
+
* `@foreignKey(Customer, { onDelete: 'CASCADE' })`.
|
|
57
|
+
*
|
|
58
|
+
* `@tenantId()` fields are excluded from the natural-key rule even though
|
|
59
|
+
* `@happyvertical/smrt-tenancy` leads a tenant-scoped class's *default*
|
|
60
|
+
* `conflictColumns` with the tenant column (#2360): the tenant column scopes
|
|
61
|
+
* ownership, it does not identify the row the way a junction's foreign key
|
|
62
|
+
* does, and it targets a class (`Tenant`) that is virtually always
|
|
63
|
+
* referenced. Without this exclusion, deleting one `Tenant` row would
|
|
64
|
+
* recursively CASCADE through every tenant-scoped table that has not
|
|
65
|
+
* declared its own `conflictColumns` — the overwhelming majority. The field
|
|
66
|
+
* is detected via the `__tenancy.isTenantIdField` marker `@tenantId()`
|
|
67
|
+
* attaches to its own registration (`FieldMeta.__tenancy`, read structurally
|
|
68
|
+
* so `smrt-core` never depends on `smrt-tenancy`). `@tenantId()` exposes no
|
|
69
|
+
* `onDelete` option today, so this cannot currently be overridden per field.
|
|
70
|
+
*
|
|
71
|
+
* @see https://github.com/happyvertical/smrt/issues/2371
|
|
72
|
+
* @module
|
|
73
|
+
*/
|
|
74
|
+
var logger = createLogger({ level: "info" });
|
|
75
|
+
/** Table holding framework-managed per-object memory entries. */
|
|
76
|
+
var CONTEXTS_TABLE = "_smrt_contexts";
|
|
77
|
+
/** Table holding framework-managed per-object embedding vectors. */
|
|
78
|
+
var EMBEDDINGS_TABLE = "_smrt_embeddings";
|
|
79
|
+
/**
|
|
80
|
+
* Reserved `_smrt_contexts.owner_id` used by `SmrtCollection.remember()` for
|
|
81
|
+
* collection-scoped memory. A real object id is a UUID and can never equal it,
|
|
82
|
+
* but the guard keeps a malformed id from wiping every sibling's defaults.
|
|
83
|
+
*/
|
|
84
|
+
var COLLECTION_OWNER_SENTINEL = "__collection__";
|
|
85
|
+
/**
|
|
86
|
+
* Maximum number of cascade levels followed from the object `delete()` was
|
|
87
|
+
* called on. A cycle between two classes that both declare `onDelete: 'CASCADE'`
|
|
88
|
+
* is broken by the visited set; this bound catches unbounded *chains*.
|
|
89
|
+
*/
|
|
90
|
+
var MAX_CASCADE_DEPTH = 10;
|
|
91
|
+
var CASCADE_ACTIONS = /* @__PURE__ */ new Set([
|
|
92
|
+
"CASCADE",
|
|
93
|
+
"SET NULL",
|
|
94
|
+
"RESTRICT",
|
|
95
|
+
"NO ACTION"
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a declared `onDelete` value.
|
|
99
|
+
*
|
|
100
|
+
* Accepts any casing and both `SET NULL` and `SET_NULL`, matching what the
|
|
101
|
+
* schema generator has always carried in the manifest. Returns `undefined` for
|
|
102
|
+
* an unset or unrecognized value so the caller can fall back to the default.
|
|
103
|
+
*/
|
|
104
|
+
function normalizeOnDelete(value) {
|
|
105
|
+
if (typeof value !== "string") return void 0;
|
|
106
|
+
const normalized = value.trim().toUpperCase().replace(/_/g, " ");
|
|
107
|
+
return CASCADE_ACTIONS.has(normalized) ? normalized : void 0;
|
|
108
|
+
}
|
|
109
|
+
function isPolymorphicAssociationClass(fields) {
|
|
110
|
+
return fields.has("metaType") && fields.has("metaId") && fields.has("role");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the cascade plan for one class.
|
|
114
|
+
*
|
|
115
|
+
* The plan is derived entirely from registry metadata, so it is valid for any
|
|
116
|
+
* database the class is used against. It is rebuilt per delete rather than
|
|
117
|
+
* cached: registration is incremental (manifests load lazily, tests register
|
|
118
|
+
* classes between cases) and a stale plan would silently skip a table.
|
|
119
|
+
*
|
|
120
|
+
* @param registry - Registry view (`ObjectRegistry` satisfies it)
|
|
121
|
+
* @param className - Class whose incoming references should be resolved
|
|
122
|
+
*/
|
|
123
|
+
function buildCascadePlan(registry, className) {
|
|
124
|
+
const targetNames = new Set(registry.getSelfReferableNames(className));
|
|
125
|
+
for (const name of [...targetNames]) {
|
|
126
|
+
const qualified = registry.getClass(name)?.qualifiedName;
|
|
127
|
+
if (qualified) targetNames.add(qualified);
|
|
128
|
+
}
|
|
129
|
+
const references = [];
|
|
130
|
+
const polymorphic = [];
|
|
131
|
+
for (const [sourceClass, relationships] of registry.getRelationshipMap()) {
|
|
132
|
+
const fields = registry.getFields(sourceClass);
|
|
133
|
+
if (isPolymorphicAssociationClass(fields)) {
|
|
134
|
+
const tableName = registry.getTableName(sourceClass);
|
|
135
|
+
if (tableName) polymorphic.push({
|
|
136
|
+
className: sourceClass,
|
|
137
|
+
tableName
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (relationships.length === 0) continue;
|
|
141
|
+
let conflictColumns;
|
|
142
|
+
for (const relationship of relationships) {
|
|
143
|
+
if (relationship.type !== "foreignKey" && relationship.type !== "crossPackageRef") continue;
|
|
144
|
+
if (!targetNames.has(relationship.targetClass)) continue;
|
|
145
|
+
const tableName = registry.getTableName(sourceClass);
|
|
146
|
+
if (!tableName) continue;
|
|
147
|
+
const column = toSnakeCase(relationship.fieldName);
|
|
148
|
+
const declaredAction = normalizeOnDelete(relationship.options?.onDelete);
|
|
149
|
+
if (!conflictColumns) conflictColumns = new Set(registry.getConflictColumns(sourceClass));
|
|
150
|
+
const isTenantIdField = relationship.options?.__tenancy?.isTenantIdField === true;
|
|
151
|
+
const action = declaredAction ?? (!isTenantIdField && conflictColumns.has(column) ? "CASCADE" : "NO ACTION");
|
|
152
|
+
if (action === "NO ACTION") continue;
|
|
153
|
+
if (action === "SET NULL" && fields.get(relationship.fieldName)?.required) throw ConfigurationError.invalidConfiguration(`${sourceClass}.${relationship.fieldName} onDelete: 'SET NULL'`, relationship.fieldName, "a nullable field (mark it `nullable: true`, or use 'CASCADE' / 'RESTRICT')");
|
|
154
|
+
references.push({
|
|
155
|
+
className: sourceClass,
|
|
156
|
+
tableName,
|
|
157
|
+
fieldName: relationship.fieldName,
|
|
158
|
+
column,
|
|
159
|
+
action,
|
|
160
|
+
declared: declaredAction !== void 0
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const metaTypes = [];
|
|
165
|
+
const qualified = registry.getClass(className)?.qualifiedName;
|
|
166
|
+
if (qualified) metaTypes.push(qualified);
|
|
167
|
+
const simple = registry.getClass(className)?.name ?? className;
|
|
168
|
+
if (!metaTypes.includes(simple)) metaTypes.push(simple);
|
|
169
|
+
return {
|
|
170
|
+
references,
|
|
171
|
+
polymorphic,
|
|
172
|
+
metaTypes,
|
|
173
|
+
isEmpty: references.length === 0 && polymorphic.length === 0
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Resolve a registry-recorded class name to its qualified form, when known. */
|
|
177
|
+
function toQualifiedClassName(registry, className) {
|
|
178
|
+
return registry.getClass(className)?.qualifiedName ?? className;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Every value `_smrt_contexts.owner_class` / `_smrt_embeddings.object_class`
|
|
182
|
+
* could plausibly hold for a row belonging to an id of `className` (review
|
|
183
|
+
* fix): both name forms (qualified + simple — older rows predate package
|
|
184
|
+
* qualification) of `className` itself, plus every other member of its STI
|
|
185
|
+
* hierarchy sharing its table. A CASCADE-collected id can belong to any
|
|
186
|
+
* concrete STI subclass of the declaring reference, which stamps its own
|
|
187
|
+
* (more specific) runtime class name at write time, not the reference's.
|
|
188
|
+
*
|
|
189
|
+
* Narrowing by this set — instead of matching by id alone — closes a
|
|
190
|
+
* cross-class collision: two unrelated classes using `idType: 'text'`
|
|
191
|
+
* (non-UUID, not guaranteed globally unique) could otherwise share an id
|
|
192
|
+
* value and have one's `remember()`/embeddings rows deleted by the other's
|
|
193
|
+
* cascade.
|
|
194
|
+
*/
|
|
195
|
+
function ownerClassCandidates(registry, className) {
|
|
196
|
+
const stiBase = registry.getSTIBase(className) ?? className;
|
|
197
|
+
const members = /* @__PURE__ */ new Set([
|
|
198
|
+
className,
|
|
199
|
+
stiBase,
|
|
200
|
+
...registry.getDescendants(stiBase)
|
|
201
|
+
]);
|
|
202
|
+
const names = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const member of members) {
|
|
204
|
+
names.add(member);
|
|
205
|
+
const registered = registry.getClass(member);
|
|
206
|
+
if (registered?.qualifiedName) names.add(registered.qualifiedName);
|
|
207
|
+
if (registered?.name) names.add(registered.name);
|
|
208
|
+
}
|
|
209
|
+
return [...names];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Build the `where` clause matching `column` against one or more ids.
|
|
213
|
+
*
|
|
214
|
+
* A single id uses equality so the planner can use a plain index; multiple ids
|
|
215
|
+
* use the adapter's `in` operator.
|
|
216
|
+
*/
|
|
217
|
+
function idPredicate(column, ids) {
|
|
218
|
+
return ids.length === 1 ? { [column]: ids[0] } : { [`${column} in`]: ids };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Run a cascade statement against a *referencing* table, tolerating the
|
|
222
|
+
* table (or an expected column) not existing in this database.
|
|
223
|
+
*
|
|
224
|
+
* The cascade plan is built from the in-process registry, which can carry a
|
|
225
|
+
* class from any imported package — including one whose table this specific
|
|
226
|
+
* database was never migrated to include (a partially adopted feature, a
|
|
227
|
+
* package pulled in for its types, or, in a test process, a fixture some
|
|
228
|
+
* other test file registered). A missing referencing table trivially has no
|
|
229
|
+
* rows to act on, so the correct behaviour is identical to the table
|
|
230
|
+
* existing and being empty — this must never abort an otherwise valid
|
|
231
|
+
* delete. Any other failure (a real constraint violation, a lock timeout, a
|
|
232
|
+
* genuine SQL error unrelated to the table's existence) still propagates.
|
|
233
|
+
*/
|
|
234
|
+
async function tolerateMissingTable(operation, fallback, context) {
|
|
235
|
+
try {
|
|
236
|
+
return await operation();
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (classifyDatabaseError(error).kind === "undefined_object") {
|
|
239
|
+
logger.warn(`Cascade delete skipped ${context.action} on '${context.table}': table or column not found in this database.`, { error: error instanceof Error ? error.message : String(error) });
|
|
240
|
+
return fallback;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function selectIds(db, tableName, column, ids) {
|
|
246
|
+
const found = [];
|
|
247
|
+
for (const batch of chunkArray(ids, 900)) {
|
|
248
|
+
const rows = await tolerateMissingTable(() => db.list(tableName, idPredicate(column, batch)), [], {
|
|
249
|
+
table: tableName,
|
|
250
|
+
action: "CASCADE select"
|
|
251
|
+
});
|
|
252
|
+
for (const row of rows) {
|
|
253
|
+
const id = row?.id;
|
|
254
|
+
if (typeof id === "string" && id.length > 0) found.push(id);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return found;
|
|
258
|
+
}
|
|
259
|
+
async function deleteByIds(db, tableName, ids) {
|
|
260
|
+
for (const batch of chunkArray(ids, 900)) await tolerateMissingTable(() => db.delete(tableName, idPredicate("id", batch)), void 0, {
|
|
261
|
+
table: tableName,
|
|
262
|
+
action: "CASCADE delete"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Remove the framework-managed side rows owned by the given object ids.
|
|
267
|
+
*
|
|
268
|
+
* `_smrt_contexts` and `_smrt_embeddings` are keyed by `(owner_class, owner_id)`
|
|
269
|
+
* and `(object_class, object_id)` respectively. Matched by id *and* class
|
|
270
|
+
* (review fix): id alone would let two unrelated classes using
|
|
271
|
+
* `idType: 'text'` (non-UUID, not guaranteed globally unique) collide on a
|
|
272
|
+
* shared id value and delete each other's memory/embeddings rows. `classNames`
|
|
273
|
+
* is the STI-hierarchy-expanded candidate set from
|
|
274
|
+
* {@link ownerClassCandidates} — the class column stores the *runtime*
|
|
275
|
+
* constructor name, which for an STI hierarchy is a concrete subclass rather
|
|
276
|
+
* than the class the cascade was planned from, so a single exact name is not
|
|
277
|
+
* enough.
|
|
278
|
+
*
|
|
279
|
+
* A missing system table is not an error — an application database may predate
|
|
280
|
+
* the table, and losing derived rows must never fail an otherwise valid delete.
|
|
281
|
+
*/
|
|
282
|
+
async function deleteSystemRows(db, ids, classNames) {
|
|
283
|
+
const ownerIds = ids.filter((id) => typeof id === "string" && id.length > 0 && id !== COLLECTION_OWNER_SENTINEL);
|
|
284
|
+
if (ownerIds.length === 0 || classNames.length === 0) return;
|
|
285
|
+
for (const [table, idColumn, classColumn] of [[
|
|
286
|
+
CONTEXTS_TABLE,
|
|
287
|
+
"owner_id",
|
|
288
|
+
"owner_class"
|
|
289
|
+
], [
|
|
290
|
+
EMBEDDINGS_TABLE,
|
|
291
|
+
"object_id",
|
|
292
|
+
"object_class"
|
|
293
|
+
]]) for (const batch of chunkArray(ownerIds, 900)) try {
|
|
294
|
+
await db.delete(table, {
|
|
295
|
+
...idPredicate(idColumn, batch),
|
|
296
|
+
...idPredicate(classColumn, classNames)
|
|
297
|
+
});
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger.warn(`Failed to clean ${table} rows during cascade delete: ${error instanceof Error ? error.message : String(error)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Resolve every reference pointing at `ids` of `className`, recursively.
|
|
304
|
+
*
|
|
305
|
+
* Does **not** delete the rows identified by `ids` — the caller owns that, so
|
|
306
|
+
* `SmrtObject.delete()` keeps issuing its own final statement and its own
|
|
307
|
+
* lifecycle hooks.
|
|
308
|
+
*/
|
|
309
|
+
async function resolveReferences(ctx, className, tableName, ids, depth) {
|
|
310
|
+
const pending = ids.filter((id) => {
|
|
311
|
+
const key = `${tableName}:${id}`;
|
|
312
|
+
if (ctx.visited.has(key)) return false;
|
|
313
|
+
ctx.visited.add(key);
|
|
314
|
+
return true;
|
|
315
|
+
});
|
|
316
|
+
if (pending.length === 0) return;
|
|
317
|
+
if (depth > MAX_CASCADE_DEPTH) throw DatabaseError.constraintViolation(`cascade delete from ${className} exceeded the maximum depth of ${MAX_CASCADE_DEPTH}; check for a chain of onDelete: 'CASCADE' references that never terminates`, className);
|
|
318
|
+
const plan = buildCascadePlan(ctx.registry, className);
|
|
319
|
+
for (const reference of plan.references) {
|
|
320
|
+
if (reference.action !== "RESTRICT") continue;
|
|
321
|
+
let remaining = 0;
|
|
322
|
+
for (const batch of chunkArray(pending, 900)) {
|
|
323
|
+
remaining += await tolerateMissingTable(() => ctx.db.count(reference.tableName, idPredicate(reference.column, batch)), 0, {
|
|
324
|
+
table: reference.tableName,
|
|
325
|
+
action: "RESTRICT check"
|
|
326
|
+
});
|
|
327
|
+
if (remaining > 0) break;
|
|
328
|
+
}
|
|
329
|
+
if (remaining > 0) throw DatabaseError.constraintViolation(`${reference.className}.${reference.fieldName} declares onDelete: 'RESTRICT' and ${remaining} row(s) still reference this ${className}`, reference.column);
|
|
330
|
+
}
|
|
331
|
+
for (const reference of plan.references) {
|
|
332
|
+
if (reference.action === "SET NULL") {
|
|
333
|
+
for (const batch of chunkArray(pending, 900)) await tolerateMissingTable(() => ctx.db.update(reference.tableName, idPredicate(reference.column, batch), { [reference.column]: null }), void 0, {
|
|
334
|
+
table: reference.tableName,
|
|
335
|
+
action: "SET NULL"
|
|
336
|
+
});
|
|
337
|
+
ctx.affectedTables.add(reference.tableName);
|
|
338
|
+
ctx.affectedTableClasses.set(reference.tableName, toQualifiedClassName(ctx.registry, reference.className));
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (reference.action !== "CASCADE") continue;
|
|
342
|
+
const childIds = await selectIds(ctx.db, reference.tableName, reference.column, pending);
|
|
343
|
+
if (childIds.length === 0) continue;
|
|
344
|
+
await resolveReferences(ctx, reference.className, reference.tableName, childIds, depth + 1);
|
|
345
|
+
await deleteSystemRows(ctx.db, childIds, ownerClassCandidates(ctx.registry, reference.className));
|
|
346
|
+
await deleteByIds(ctx.db, reference.tableName, childIds);
|
|
347
|
+
ctx.affectedTables.add(reference.tableName);
|
|
348
|
+
ctx.affectedTableClasses.set(reference.tableName, toQualifiedClassName(ctx.registry, reference.className));
|
|
349
|
+
}
|
|
350
|
+
for (const association of plan.polymorphic) for (const batch of chunkArray(pending, 900)) {
|
|
351
|
+
const where = { ...idPredicate("meta_id", batch) };
|
|
352
|
+
if (plan.metaTypes.length === 1) where.meta_type = plan.metaTypes[0];
|
|
353
|
+
else where["meta_type in"] = plan.metaTypes;
|
|
354
|
+
if (((await tolerateMissingTable(() => ctx.db.delete(association.tableName, where), void 0, {
|
|
355
|
+
table: association.tableName,
|
|
356
|
+
action: "polymorphic association cleanup"
|
|
357
|
+
}))?.affected ?? 0) > 0) {
|
|
358
|
+
ctx.affectedTables.add(association.tableName);
|
|
359
|
+
ctx.affectedTableClasses.set(association.tableName, toQualifiedClassName(ctx.registry, association.className));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Resolve every reference to `ids` of `className` and clean their framework
|
|
365
|
+
* side rows, then hand control back so the caller can delete the rows.
|
|
366
|
+
*
|
|
367
|
+
* Callers are expected to run this on a transaction-bound `db` — see
|
|
368
|
+
* {@link runCascadeDelete}, which owns that decision.
|
|
369
|
+
*/
|
|
370
|
+
async function cascadeReferencesTo(db, registry, target) {
|
|
371
|
+
const ctx = {
|
|
372
|
+
db,
|
|
373
|
+
registry,
|
|
374
|
+
visited: /* @__PURE__ */ new Set(),
|
|
375
|
+
affectedTables: /* @__PURE__ */ new Set(),
|
|
376
|
+
affectedTableClasses: /* @__PURE__ */ new Map()
|
|
377
|
+
};
|
|
378
|
+
await resolveReferences(ctx, target.className, target.tableName, target.ids, 0);
|
|
379
|
+
await deleteSystemRows(db, target.ids, ownerClassCandidates(registry, target.className));
|
|
380
|
+
return {
|
|
381
|
+
affectedTables: ctx.affectedTables,
|
|
382
|
+
affectedTableClasses: ctx.affectedTableClasses
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Run the cascade and the target row's own deletion atomically.
|
|
387
|
+
*
|
|
388
|
+
* When anything references the class and the adapter exposes `transaction()`,
|
|
389
|
+
* the whole sequence runs inside one — including the caller's `deleteSelf`
|
|
390
|
+
* statement, so a failure part-way through cannot leave the object deleted with
|
|
391
|
+
* its junction rows intact (or vice versa). Adapters without transaction
|
|
392
|
+
* support run the same statements sequentially; this is the documented
|
|
393
|
+
* degradation, not a silent one.
|
|
394
|
+
*
|
|
395
|
+
* When nothing references the class there is nothing to keep consistent, and
|
|
396
|
+
* the transaction is skipped — see the comment on that branch.
|
|
397
|
+
*
|
|
398
|
+
* @param db - Database the object is bound to
|
|
399
|
+
* @param registry - Registry view used to build cascade plans
|
|
400
|
+
* @param target - Class, table and id of the object being deleted
|
|
401
|
+
* @param deleteSelf - Issues the target row's own `DELETE`, on the tx-bound db
|
|
402
|
+
* @returns Tables affected by the cascade, for read-cache invalidation
|
|
403
|
+
*/
|
|
404
|
+
async function runCascadeDelete(db, registry, target, deleteSelf) {
|
|
405
|
+
const ids = target.id ? [target.id] : [];
|
|
406
|
+
if (buildCascadePlan(registry, target.className).isEmpty) {
|
|
407
|
+
await deleteSelf(db);
|
|
408
|
+
await deleteSystemRows(db, ids, ownerClassCandidates(registry, target.className));
|
|
409
|
+
return {
|
|
410
|
+
affectedTables: /* @__PURE__ */ new Set(),
|
|
411
|
+
affectedTableClasses: /* @__PURE__ */ new Map()
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const run = async (bound) => {
|
|
415
|
+
const result = await cascadeReferencesTo(bound, registry, {
|
|
416
|
+
className: target.className,
|
|
417
|
+
tableName: target.tableName,
|
|
418
|
+
ids
|
|
419
|
+
});
|
|
420
|
+
await deleteSelf(bound);
|
|
421
|
+
return result;
|
|
422
|
+
};
|
|
423
|
+
const transaction = db.transaction;
|
|
424
|
+
if (typeof transaction !== "function") return run(db);
|
|
425
|
+
return transaction.call(db, run);
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
export { buildCascadePlan, cascadeReferencesTo, normalizeOnDelete, runCascadeDelete };
|
|
429
|
+
|
|
430
|
+
//# sourceMappingURL=cascade.js.map
|