@happyvertical/smrt-core 0.49.3 → 0.49.5

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.
Files changed (156) hide show
  1. package/README.md +20 -0
  2. package/agents/change-feed.md +19 -0
  3. package/agents/collection-reads.md +21 -0
  4. package/agents/generators.md +5 -0
  5. package/agents/memory.md +23 -3
  6. package/agents/null-equal-indexes.md +88 -0
  7. package/agents/object-runtime.md +85 -0
  8. package/agents/schema-paths.md +7 -7
  9. package/dist/browser.js +2 -2
  10. package/dist/cascade.d.ts.map +1 -1
  11. package/dist/cascade.js +13 -10
  12. package/dist/cascade.js.map +1 -1
  13. package/dist/change-feed.d.ts +22 -0
  14. package/dist/change-feed.d.ts.map +1 -1
  15. package/dist/change-feed.js +147 -6
  16. package/dist/change-feed.js.map +1 -1
  17. package/dist/class.js +1 -1
  18. package/dist/collection-cache.d.ts +10 -1
  19. package/dist/collection-cache.d.ts.map +1 -1
  20. package/dist/collection-cache.js +65 -10
  21. package/dist/collection-cache.js.map +1 -1
  22. package/dist/collection.d.ts +69 -2
  23. package/dist/collection.d.ts.map +1 -1
  24. package/dist/collection.js +142 -9
  25. package/dist/collection.js.map +1 -1
  26. package/dist/consumer-plugin/artifact-publication.d.ts +27 -0
  27. package/dist/consumer-plugin/artifact-publication.d.ts.map +1 -0
  28. package/dist/consumer-plugin/artifact-publication.js +80 -0
  29. package/dist/consumer-plugin/artifact-publication.js.map +1 -0
  30. package/dist/consumer-plugin/index.d.ts.map +1 -1
  31. package/dist/consumer-plugin/index.js +44 -6
  32. package/dist/consumer-plugin/index.js.map +1 -1
  33. package/dist/db-errors.js +1 -1
  34. package/dist/db-errors.js.map +1 -1
  35. package/dist/embedded-write-queue.d.ts +1 -1
  36. package/dist/embedded-write-queue.d.ts.map +1 -1
  37. package/dist/embedded-write-queue.js +16 -6
  38. package/dist/embedded-write-queue.js.map +1 -1
  39. package/dist/embeddings/storage.d.ts +16 -0
  40. package/dist/embeddings/storage.d.ts.map +1 -1
  41. package/dist/embeddings/storage.js +40 -0
  42. package/dist/embeddings/storage.js.map +1 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -3
  46. package/dist/interceptors.d.ts +22 -0
  47. package/dist/interceptors.d.ts.map +1 -1
  48. package/dist/interceptors.js +12 -0
  49. package/dist/interceptors.js.map +1 -1
  50. package/dist/junction.d.ts +12 -3
  51. package/dist/junction.d.ts.map +1 -1
  52. package/dist/junction.js +34 -4
  53. package/dist/junction.js.map +1 -1
  54. package/dist/knowledge-config.d.ts +6 -0
  55. package/dist/knowledge-config.d.ts.map +1 -0
  56. package/dist/knowledge-config.js +48 -0
  57. package/dist/knowledge-config.js.map +1 -0
  58. package/dist/manifest/static-manifest.d.ts.map +1 -1
  59. package/dist/manifest/static-manifest.js +91 -1
  60. package/dist/manifest/static-manifest.js.map +1 -1
  61. package/dist/manifest/store.js +1 -1
  62. package/dist/manifest/store.js.map +1 -1
  63. package/dist/manifest.json +104 -1
  64. package/dist/migrations/differ.d.ts.map +1 -1
  65. package/dist/migrations/differ.js +2 -0
  66. package/dist/migrations/differ.js.map +1 -1
  67. package/dist/migrations/generator.d.ts.map +1 -1
  68. package/dist/migrations/generator.js +2 -0
  69. package/dist/migrations/generator.js.map +1 -1
  70. package/dist/migrations/index.d.ts +1 -0
  71. package/dist/migrations/index.d.ts.map +1 -1
  72. package/dist/migrations/index.js +2 -1
  73. package/dist/migrations/null-equal-indexes.d.ts +37 -0
  74. package/dist/migrations/null-equal-indexes.d.ts.map +1 -0
  75. package/dist/migrations/null-equal-indexes.js +133 -0
  76. package/dist/migrations/null-equal-indexes.js.map +1 -0
  77. package/dist/migrations/tracker.d.ts.map +1 -1
  78. package/dist/migrations/tracker.js +6 -0
  79. package/dist/migrations/tracker.js.map +1 -1
  80. package/dist/migrations.js +2 -1
  81. package/dist/object.d.ts +33 -0
  82. package/dist/object.d.ts.map +1 -1
  83. package/dist/object.js +316 -46
  84. package/dist/object.js.map +1 -1
  85. package/dist/plain-json.d.ts +6 -0
  86. package/dist/plain-json.d.ts.map +1 -0
  87. package/dist/plain-json.js +41 -0
  88. package/dist/plain-json.js.map +1 -0
  89. package/dist/postgres-permissions.d.ts.map +1 -1
  90. package/dist/postgres-permissions.js +6 -2
  91. package/dist/postgres-permissions.js.map +1 -1
  92. package/dist/prebuild/index.d.ts.map +1 -1
  93. package/dist/prebuild/index.js +3 -3
  94. package/dist/prebuild/index.js.map +1 -1
  95. package/dist/registry/class-registration.d.ts.map +1 -1
  96. package/dist/registry/class-registration.js +4 -1
  97. package/dist/registry/class-registration.js.map +1 -1
  98. package/dist/registry/schema-builder.d.ts.map +1 -1
  99. package/dist/registry/schema-builder.js +4 -3
  100. package/dist/registry/schema-builder.js.map +1 -1
  101. package/dist/registry/validator.d.ts +1 -0
  102. package/dist/registry/validator.d.ts.map +1 -1
  103. package/dist/registry/validator.js +8 -1
  104. package/dist/registry/validator.js.map +1 -1
  105. package/dist/registry.js +1 -1
  106. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  107. package/dist/scanner/manifest-generator.js +21 -0
  108. package/dist/scanner/manifest-generator.js.map +1 -1
  109. package/dist/scanner/types.d.ts +2 -0
  110. package/dist/scanner/types.d.ts.map +1 -1
  111. package/dist/scanner/types.js.map +1 -1
  112. package/dist/schema/conflict-target.d.ts +8 -2
  113. package/dist/schema/conflict-target.d.ts.map +1 -1
  114. package/dist/schema/conflict-target.js +11 -3
  115. package/dist/schema/conflict-target.js.map +1 -1
  116. package/dist/schema/ddl/null-equal-index.d.ts +5 -0
  117. package/dist/schema/ddl/null-equal-index.d.ts.map +1 -0
  118. package/dist/schema/ddl/null-equal-index.js +17 -0
  119. package/dist/schema/ddl/null-equal-index.js.map +1 -0
  120. package/dist/schema/ddl/postgres-strategy.d.ts +2 -1
  121. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  122. package/dist/schema/ddl/postgres-strategy.js +7 -0
  123. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  124. package/dist/schema/generator.d.ts.map +1 -1
  125. package/dist/schema/generator.js +8 -3
  126. package/dist/schema/generator.js.map +1 -1
  127. package/dist/schema/manifest-schema.d.ts +2 -1
  128. package/dist/schema/manifest-schema.d.ts.map +1 -1
  129. package/dist/schema/manifest-schema.js +12 -4
  130. package/dist/schema/manifest-schema.js.map +1 -1
  131. package/dist/schema/schema-manager.d.ts.map +1 -1
  132. package/dist/schema/schema-manager.js +1 -1
  133. package/dist/schema/schema-manager.js.map +1 -1
  134. package/dist/schema/types.d.ts +2 -0
  135. package/dist/schema/types.d.ts.map +1 -1
  136. package/dist/scripts/browser-plain-json-regression.mjs +85 -0
  137. package/dist/smrt-knowledge.json +73 -15
  138. package/dist/system/schema.d.ts +14 -3
  139. package/dist/system/schema.d.ts.map +1 -1
  140. package/dist/system/schema.js +120 -2
  141. package/dist/system/schema.js.map +1 -1
  142. package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
  143. package/dist/vite-plugin/api-client-entries.js +9 -7
  144. package/dist/vite-plugin/api-client-entries.js.map +1 -1
  145. package/dist/vite-plugin/index.d.ts +24 -0
  146. package/dist/vite-plugin/index.d.ts.map +1 -1
  147. package/dist/vite-plugin/index.js +48 -115
  148. package/dist/vite-plugin/index.js.map +1 -1
  149. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  150. package/dist/vite-plugin/sveltekit-generator.js +2 -2
  151. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  152. package/dist/vite-plugin/web-collections.d.ts +18 -0
  153. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  154. package/dist/vite-plugin/web-collections.js +55 -2
  155. package/dist/vite-plugin/web-collections.js.map +1 -1
  156. package/package.json +5 -4
package/README.md CHANGED
@@ -191,6 +191,22 @@ generates routes, types, registration, and virtual modules, but it disables
191
191
  source/package scans, watch rescans, and manifest writes. Omit
192
192
  `generationSnapshot` for normal local development.
193
193
 
194
+ #### Consumer web collection definitions
195
+
196
+ `smrtConsumer()` also resolves `@smrt/web` for browser collection consumers:
197
+
198
+ ```typescript
199
+ import { collectionDefinitions, getCollectionDefinition } from '@smrt/web';
200
+ ```
201
+
202
+ The module projects the consumer's aggregated dependency manifest through the
203
+ same web-definition generator as `smrtPlugin()`. Each definition retains its
204
+ provider-qualified `objectRef` (for example, `@acme/widgets:Widget`) and its
205
+ manifest-derived fields, actions, relationships, WebMCP descriptors, and
206
+ `manifestHash`. Consumer snapshots select the dependency view, so local project
207
+ objects are not included. During declaration generation the matching
208
+ `smrt-web.d.ts` module is written alongside the other consumer virtual modules.
209
+
194
210
  ### Generated SvelteKit routes
195
211
 
196
212
  Enable SvelteKit route generation with `svelteKit: { enabled: true }`. Its
@@ -267,6 +283,10 @@ Because s-m-r-t owns every mutation path (`save()`, `delete()`,
267
283
  automatically invalidates the affected table's cached entries in-process —
268
284
  including STI siblings sharing the table. Cached values are raw rows:
269
285
  hydration and read interceptors (tenancy, audit) still run on every call.
286
+ Concurrent identical misses coalesce only on the same concrete database
287
+ interface. Distinct connection or transaction interfaces never share pending
288
+ reads or completed rows, even with the same URL; invalidation still clears
289
+ all interfaces for that URL and table.
270
290
 
271
291
  Caches are per-process. For multi-replica deployments, add
272
292
  `crossProcess: true` to broadcast invalidations over the database adapter's
@@ -15,3 +15,22 @@ Adapter-agnostic change-observation spine (`src/change-feed.ts`) — the server
15
15
  - `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table **plus that table's staged-but-undrained count**, so a staged write still moves the ETag and cannot false-304 a client; the sum is monotonic because draining `n` staged rows raises the table's `MAX(seq)` by at least `n`, and both terms are read in ONE statement — separate reads let a drain be counted twice, minting a version a later write re-mints; replica-stable, with no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.
16
16
  - Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error, and `resyncCursor` is the resume cursor after the client completes a full refetch.
17
17
  - Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows?, dryRun? })` — scheduled since #2375 by `runRetentionSweep()` (30-day default), so nothing needs to call it directly; `dryRun` counts the same predicate instead of deleting. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable. The age bound is a **prefix** bound — everything below the oldest entry still inside the window — because `created_at` and `seq` are not co-monotonic (writer clocks skew, and a staged entry carries its stage-time stamp into a later-assigned sequence); deleting by timestamp alone could punch a hole in the middle of the retained run, where `since < floor - 1` cannot see it and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.
18
+
19
+
20
+ ## Compatible bulk mutations (#2818)
21
+
22
+ `appendChanges(db, entries)` validates all inputs before writing and returns one
23
+ sequence (or PostgreSQL staged `null`) per entry in input order. A nonempty batch
24
+ uses one client SQL statement and one set-based insert. The PostgreSQL
25
+ `_smrt_append_changes(jsonb)` helper chooses the staged/direct path once before
26
+ its own writes assign a transaction ID, and retains the same exception isolation
27
+ as the single-row helper. Bootstrap refreshes its body marker and runtime grants
28
+ include this exact helper signature.
29
+
30
+ The registered feed interceptor explicitly supports grouped lifecycle completion.
31
+ `recordInstanceChanges()` retains observable-table filtering, row/tenant IDs,
32
+ per-row operations, and best-effort failure handling. It publishes each direct
33
+ sequence in order; staged entries publish through the existing committed drain.
34
+ A batch is never replaced with a table-level bump or a single aggregate event.
35
+ The junction caller bounds its batch; the low-level append API does not split
36
+ one logical append into partially committed chunks.
@@ -68,3 +68,24 @@ the first error.
68
68
  `=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, and `like`. Arrays imply `IN`,
69
69
  and null values render `IS NULL`/`IS NOT NULL`. `contains` and dot-notation JSON
70
70
  paths are intentionally rejected until the SQL layer supports them.
71
+
72
+ For subclass-owned bounded SQL, `resolveListReadPredicate()` provides SQL scope
73
+ plus `finish(instances)`. Call `finish` exactly once on the final hydrated page
74
+ (including empty pages), after raw-query hooks and annotations, outside provider
75
+ fallback catches. It invokes normal `afterList` using the original `beforeList`
76
+ context and model identity. It does not fetch replacement rows after filtering.
77
+ ID-only consumers have no hydrated page and do not call this completion step.
78
+
79
+ `semanticSearchIdsWithAvailability()` is the protected fallback-aware boundary:
80
+ only absent embedding configuration or a failed `provider.embed()` returns
81
+ `available: false`. Invalid options, authorization, ranking, query, and caller
82
+ override failures propagate regardless of their error class. Public
83
+ `semanticSearchIds()` keeps its array/throw contract by throwing the returned
84
+ provider error. Consumers must branch on availability, never classify thrown
85
+ `EmbeddingUnavailableError` values from an entire search as provider failures.
86
+
87
+ Semantic ID eligibility binds its complete beforeList/STI/where predicate once
88
+ in a CTE per candidate chunk. Only the scalar eligibility mask leaves the
89
+ application database. Candidate chunks use the remaining conservative 999-bind
90
+ budget (minimum 1); a predicate already larger than that budget retains the
91
+ adapter’s existing capacity instead of being rejected by a new API limit.
@@ -26,6 +26,11 @@ collection class and its populated model share an endpoint, the model owns the
26
26
  canonical collection key and row payload schema; the collection class remains
27
27
  available under a deterministic class-derived secondary key. Selection and
28
28
  collision suffixes must not depend on manifest insertion order (#2027).
29
+ Route emission iterates manifest objects in the same deterministic
30
+ qualified-identity order (`orderedManifestObjectEntries`), so when two models
31
+ share one collection the last writer of each route file — and therefore the
32
+ CRUD method set `resolveGeneratedEndpointCrudMethods` mirrors into
33
+ `smrt-client.d.ts` — is stable across scan orders (#2754).
29
34
  For aggregated manifests, inheritance and item-type references resolve exact
30
35
  qualified names first, then package-local simple names, then a stable identity
31
36
  fallback so duplicate class names across packages cannot reintroduce ordering.
package/agents/memory.md CHANGED
@@ -11,10 +11,30 @@ filtering; object/collection recall does not update them.
11
11
 
12
12
  Semantic search uses `_smrt_embeddings` and cosine ranking over fields declared
13
13
  by `@smrt({ embeddings })`, with native pgvector/HNSW or an in-memory fallback.
14
- Results hydrate through `list({ 'id in': … })`, so normal tenant isolation still
15
- applies. Keep injected search behind the `SmrtCollection.semanticSearch`
16
- boundary.
14
+ Legacy `semanticSearch` results hydrate through `list({ 'id in': … })`, so normal
15
+ tenant isolation still applies. Keep injected search behind the public
16
+ `SmrtCollection.semanticSearch` / `semanticSearchIds` boundaries.
17
17
 
18
18
  `LearningMemory.capture()` reinforces successes and decays failures while
19
19
  updating outcome counters. Its tenant-isolated `recall()` applies confidence,
20
20
  expiry, time-decay, and hierarchical-scope filters and refreshes `last_used_at`.
21
+
22
+ `semanticSearchIds(query, options)` and `findSimilarIdsToEmbedding(vector, options)`
23
+ return `{ id, similarity }` without object hydration. They apply caller `where`,
24
+ normal `beforeList` tenancy predicates, and child STI scope **before** exact
25
+ cosine top-K (legacy `semanticSearch` applies caller `where` after ranking).
26
+ Equal scores sort by object ID. Search scans JSON embedding vectors in keyset
27
+ batches of 64, including when native vector storage is enabled (JSON is always
28
+ persisted); each batch checks application eligibility using a fixed-size scalar
29
+ SQL mask of primary-key `EXISTS` probes. This supports separate system/app
30
+ databases without fetching all tenant IDs or application rows. Memory is
31
+ O(limit + 64); the exact fallback still scans all matching stored embeddings.
32
+ The embedding primary key supports the cursor and application primary keys
33
+ support eligibility probes; no new application index is needed. Pagination
34
+ callers can request offset + limit scored IDs, then hydrate only their page.
35
+
36
+ `semanticSearchIds` reports missing embedding configuration/provider failure as
37
+ `EmbeddingUnavailableError`; authorization/interceptor and application SQL errors
38
+ remain distinct and must not be caught as text-fallback signals. Subclass SQL
39
+ readers can compile normal beforeList + STI predicates with the protected
40
+ `resolveListReadPredicate()` (ordered values and portable `?` placeholders).
@@ -0,0 +1,88 @@
1
+ # Nullable framework conflict identities
2
+
3
+ `SmrtObject.save()` and the SDK upsert already treat NULL conflict values as
4
+ identical. On PostgreSQL 15+, a matching full `UNIQUE NULLS NOT DISTINCT` index
5
+ allows a warm nullable upsert to execute one native statement. This statement
6
+ bound excludes the first server/index capability probes and object lifecycle
7
+ work. PostgreSQL version alone does not establish readiness.
8
+
9
+ SMRT marks only its generated **runtime conflict identity** indexes with
10
+ `nullsNotDistinct: true`, and only when a key column is nullable. This includes
11
+ CTI/STI defaults and explicitly configured conflict columns. Primary-key targets
12
+ need no additional index. Optional business-field and declared composite unique
13
+ indexes remain ordinary NULLS DISTINCT; multiple NULL business values continue
14
+ to be legal. The marker survives manifest, registry and migration schema paths.
15
+
16
+ Fresh PostgreSQL DDL uses a version-gated dynamic `DO` block: PostgreSQL 15+
17
+ executes NND, while older servers execute ordinary UNIQUE without parsing the
18
+ unsupported syntax. SQLite/DuckDB DDL and their existing upsert paths do not
19
+ change. The PostgreSQL statement planner preserves the complete block and
20
+ **rejects `--postgres-safe`** when such a block is present, before executing the
21
+ plan. Schedule an atomic maintenance window and retry ordinary `db:migrate`
22
+ without that flag. No conditional blocking index build runs in concurrent mode.
23
+
24
+ ## Existing deployment procedure
25
+
26
+ 1. Generate current manifests and run ordinary schema migration to establish
27
+ the current tables/conflict column shapes. Ordinary parity does not replace
28
+ an existing index merely because its NULL semantics differ.
29
+ 2. Run `smrt db:migrate-null-equal-indexes --dry-run` against the intended
30
+ database. The command discovers framework schema markers, checks server
31
+ version and exact public-schema index catalogs, and prints readiness and
32
+ replacement SQL. It performs no DDL or data writes in dry-run mode.
33
+ 3. Resolve blocked findings explicitly. Duplicate NULL-equal groups are counted
34
+ without printing their values; the report supplies a quoted detector SELECT.
35
+ Decide correct identities with the data owner. This command never deletes or
36
+ merges rows. Missing/drifted indexes require explicit schema repair first. PostgreSQL's
37
+ physical truncation of a generated name is recognized using its server
38
+ `name` type, including multibyte names. A differently renamed equivalent is
39
+ never adopted automatically: ordinary `db:migrate` may already consider it
40
+ satisfied and emit no repair. Inspect `pg_indexes`, verify framework ownership,
41
+ exact ordered keys, uniqueness and dependencies, then in a separate maintenance
42
+ transaction rename that verified owned index with `ALTER INDEX ... RENAME TO`
43
+ the expected name, or create the expected index from generated schema. Never
44
+ rename an unrelated business index. Repeat preflight after the repair.
45
+ 4. Schedule a maintenance window and run `smrt db:migrate-null-equal-indexes`.
46
+ It acquires ACCESS EXCLUSIVE table locks in deterministic order, repeats
47
+ catalog and duplicate checks under those locks, and replaces pending indexes
48
+ in one transaction. Writers wait while the locks are held. Lock/statement
49
+ timeouts use `migrations.postgres` settings (30s/60s defaults, 0 disables).
50
+ 5. Refresh database connections in **every application process**, or restart
51
+ them. The SDK caches negative capability probes per adapter. Use the public
52
+ `getDatabase({ ...sameOptions, clearCache: true })` API and replace consumer
53
+ references to the old adapter; merely refreshing the CLI process cannot
54
+ refresh running applications. Until refresh, the existing fallback remains
55
+ correct, but retains its additional statements.
56
+ 6. Repeat dry-run to verify current indexes. A repeated apply skips current
57
+ indexes. PostgreSQL <15 and other engines report unsupported and change
58
+ nothing; upgrading PostgreSQL does not itself replace an ordinary index.
59
+
60
+ The migration refuses constraint-owned indexes, reverse dependencies (including
61
+ foreign keys), extension/internal ownership, partitioned tables, predicates,
62
+ expressions, INCLUDE columns, nondefault ordering/operator classes/collation or
63
+ storage, and changed key order. These require a separate dependency-aware
64
+ migration. It never uses CASCADE or drops foreign keys. A failed locked recheck,
65
+ DDL statement or timeout rolls back every replacement and retains the original
66
+ indexes; resolve the reported cause and retry. Catalog state supplies the
67
+ idempotency/readiness record, rather than an audit marker that could falsely
68
+ claim readiness after later DDL drift. Keep the command output in the operator's
69
+ change record. No runtime object operation creates or upgrades application DDL.
70
+
71
+ Implementation: `schema/conflict-target.ts`, `schema/ddl/null-equal-index.ts`,
72
+ `migrations/null-equal-indexes.ts`, and CLI `db-migrate-null-equal-indexes.ts`.
73
+
74
+ Checked-in `src/manifest/manifest.json` files are supported source-only fallbacks,
75
+ including packages imported before their first build. Regenerate them through
76
+ `ManifestBuilder.generate({ outputDir: 'src/manifest', outputName: 'manifest.json',
77
+ loadViteConfig: false, include: ['src/**/*.ts'], exclude: ['**/*.test.ts',
78
+ '**/*.spec.ts', '**/__tests__/**'], discoverExternalPackages: true,
79
+ injectPackageInfo: true })` from each package directory when schema generation
80
+ changes. This is the canonical source and build writer, not a marker-only JSON
81
+ patch. Same-name collected indexes must have compatible ordered columns,
82
+ uniqueness, predicates and expressions; otherwise collection fails with a
83
+ regeneration diagnostic. A compatible optional ownership marker survives either
84
+ contributor order; explicitly contradictory markers also fail closed.
85
+
86
+ Collection access classes retain schema metadata copied from their resolved item
87
+ model after model generation completes. They never generate an independent
88
+ conflict identity; indirect collection subclasses inherit the same item schema.
@@ -19,6 +19,19 @@
19
19
  persisted rows must use `getCanonicalPersistedRow()` so UUID identities are
20
20
  cast in the same coherent read before reuse.
21
21
  - `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a "content body" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)
22
+ - `normalizePersistenceData(data)` is the synchronous final derived-column hook:
23
+ shared save preparation passes read-only snake-case row data after the complete polymorphic
24
+ `toJSON()` / `transformJSON()` chain and UUID coercion, then merges the returned
25
+ columns before every insert/update/upsert branch. Declare permitted snake-case
26
+ columns with `getPersistenceDerivedColumns()`, preserving super declarations.
27
+ Declarations must name registered schema fields; framework identity, tenant,
28
+ revision, STI metadata and natural conflict columns are always rejected.
29
+ Undeclared return keys reject before persistence. The hook receives a frozen
30
+ shallow row copy. Derive only the declared columns; do not perform I/O.
31
+ Preserve `super` results. It does not alter plain/public serialization;
32
+ use `transformJSON()` for that existing contract. Ordinary saves and eligible
33
+ junction batches share this preparation. A custom normalization override makes
34
+ a junction ineligible for batching, preserving its virtual per-row save path.
22
35
  - `save()` error contract (#2366): unique/PK violation → `ValidationError` `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL → `VALIDATION_REQUIRED_FIELD`, both on the first attempt on every adapter; any other database failure → `DatabaseError` with the driver error on `cause`
23
36
  - `getSlug()`: auto-generates from name → title → label → id
24
37
  - `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)
@@ -31,6 +44,25 @@ legacy external string targets retain manifest discovery. Junction column naming
31
44
  conventions still use display names. Cached reads retain their tenant rechecks.
32
45
 
33
46
 
47
+ ## Plain-object serialization
48
+
49
+ `toPlainObject()` materializes the `toJSON()` / `transformJSON()` result directly,
50
+ without an intermediate JSON string for ordinary payloads. Native `JSON.rawJSON()`
51
+ literals are decoded once with `JSON.parse()`. Nested hooks and JSON omission/coercion
52
+ rules still apply. The active ancestor stack rejects cycles while allowing
53
+ shared siblings, which become independent plain copies. Arrays skip boxed-value
54
+ brand checks; other objects use intrinsic brand checks without reading user
55
+ `Symbol.toStringTag` getters. Customize the payload through `transformJSON()`.
56
+ The focused `to-plain-object.test.ts` suite compares legacy output and reports
57
+ warmed, interleaved per-row timings without a flaky timing assertion.
58
+
59
+ `pnpm --filter @happyvertical/smrt-core test:browser` bundles the production
60
+ `src/plain-json.ts` helper and executes it in Chromium with `process` and
61
+ `JSON.isRawJSON` unavailable. It is an opt-in local/browser release gate rather
62
+ than part of the default core suite: it requires the repository's
63
+ `playwright-core` dependency and an installed Chromium binary. The normal test
64
+ suite covers the same helper through `toPlainObject()` on Node.
65
+
34
66
  ## SmrtCollection Query
35
67
 
36
68
  Projection, latest-related, facets, counts, and bounded read plans are
@@ -62,6 +94,59 @@ explicit wildcards. Adding operators requires SQL support first.
62
94
  STI child collections auto-filter by `_meta_type`. Query bounds — `LIMIT 1` on `get()`, the `limit`/`offset` parser, the `orderBy` whitelist and sensitive/permission refusals, and the deterministic generated-list ordering (#2367) — are in [query-bounds.md](query-bounds.md).
63
95
 
64
96
 
97
+ ## Junction reads and compatible writes
98
+
99
+ `byLeft(id, { relationship: 'attachment', limit: 20, offset: 40 })` and
100
+ `byRight()` forward `limit`/`offset` to the collection's existing bounds parser;
101
+ other options remain field filters. Pagination is read-only: `detach()` still
102
+ requires field filters and never deletes a page. Cursor pagination is not part
103
+ of this API; callers can use deterministic limit/offset pages.
104
+
105
+ `setLinks()` preserves replacement semantics: even retained right IDs get new
106
+ junction IDs/timestamps and delete/create change entries. `detach()` and
107
+ `setLinks()` batch compatible models automatically. Within 100 removed + added
108
+ rows and 900 inserted bind values, a warm SQLite/DuckDB/PostgreSQL operation uses
109
+ constant framework SQL: one snapshot, grouped owned-memory cleanup and delete,
110
+ one multi-row natural-key upsert, and one feed append per nonempty mutation
111
+ phase. Bootstrap, retries, and caller-defined work are excluded. The read
112
+ snapshot and in-memory work remain proportional to link count.
113
+
114
+ Eligibility is deliberately conservative and rechecked each call: base runtime
115
+ methods/accessors and collection create/attach behavior, one non-STI table,
116
+ compiler-owned declarative validators, no validated cross-package references,
117
+ no embedding generation, no incoming typed references, non-NULL unique
118
+ conflict values with no duplicate input conflict keys, and explicit consent
119
+ from every mutation interceptor. Otherwise the original virtual per-row path
120
+ runs. Larger sets retain that fallback; there is no truncated replacement.
121
+ Polymorphic association cleanup remains grouped through the owning cascade API
122
+ and invalidates every affected table. JSON export adapters retain their ordinary
123
+ lifecycle; unrecognized transaction-only DuckDB handles also fall back.
124
+ The tenancy interceptor consents only without directory dispatch/custom error
125
+ callbacks. Tenant checks and auto-population still run per row. The change-feed
126
+ interceptor appends every row and preserves tenant IDs and tombstones.
127
+
128
+ Bulk lifecycle preparation and completion are owned by `SmrtObject`; collection
129
+ initialization shares `createUnsaved()` with `create()`. Additional interceptors
130
+ may supply `bulkMutation.compatible()` only if grouped before/persist/after
131
+ phases preserve their behavior and before hooks have no side effects beyond
132
+ instance/context mutation. Preparation can discover an unsupported shape and
133
+ fall back; before hooks must tolerate that rehearsal. Unknown interceptors never
134
+ opt in implicitly.
135
+ The compiler marks its own callback-free validators by function identity.
136
+
137
+ The whole replacement remains nontransactional unless the caller supplies a
138
+ transaction. A delete group is atomic with its owned-memory cleanup and each
139
+ multi-row insert is atomic, so database failure can leave the deletion phase
140
+ committed without new links. No per-row prefix is promised for a failed batch.
141
+ The batch delete reuses an existing DuckDB transaction when the SDK explicitly
142
+ refuses savepoint nesting before running any work; it never replays a callback.
143
+ The legacy fallback retains its existing detectable nested-transaction refusal
144
+ for ordinary deletes inside caller-owned DuckDB transactions
145
+ ([#2824](https://github.com/happyvertical/smrt/issues/2824)).
146
+ Public ordinary `save()` still owns revision CAS; batches only create new
147
+ instances and never use this path to overwrite a loaded revision.
148
+
149
+
65
150
  ## DispatchBus
66
151
 
67
152
  - `emit(signalType, payload, metadata)` → creates persistent Dispatch record
@@ -74,13 +74,13 @@ divergence is a bug in the generator, not an exception to add to the test.
74
74
  migration replaces a same-name global unique with tenant-led columns. That
75
75
  prefix serves tenant and tenant-scoped slug reads; a legacy standalone tenant
76
76
  index is dropped only with `--drop-indexes`.
77
- - **Optional NULL tenants** dedup through SDK null-aware upsert (PostgreSQL
78
- `IS NOT DISTINCT FROM` plus advisory lock; SQLite process lock), not the
79
- unique index: raw SQL can duplicate NULL-tenant keys. Raw global inserts need
80
- `WHERE NOT EXISTS` and a PostgreSQL advisory lock; an old global `ON CONFLICT`
81
- target no longer binds. Save serializes an unset tenant explicitly as NULL,
82
- because every conflict column must be present. PostgreSQL `NULLS NOT DISTINCT`
83
- remains a potential follow-up, not current enforcement.
77
+ - **Optional NULL tenants** retain the SDK's NULL-equal upsert identity. Generated
78
+ framework conflict indexes with nullable keys carry `nullsNotDistinct: true`;
79
+ PostgreSQL 15+ creates `UNIQUE NULLS NOT DISTINCT`, enabling warm single-statement
80
+ native upserts. Earlier PostgreSQL, SQLite and DuckDB retain adapter fallback
81
+ semantics. Existing ordinary indexes are upgraded only through the explicit
82
+ [NULL-equal maintenance migration](null-equal-indexes.md). Optional business
83
+ unique fields remain NULLS DISTINCT; this marker is not inferred from them.
84
84
  - **Tenant-key rollout requires a maintenance window.** Old code/new indexes
85
85
  and new code/old indexes both fail new-object saves because conflict column
86
86
  sets must match exactly; persisted ID-based saves still work. Backfill legacy
package/dist/browser.js CHANGED
@@ -17,7 +17,7 @@ import { SmrtObject } from "./object.js";
17
17
  import { isFrameworkBaseClass } from "./registry/framework-base-classes.js";
18
18
  import { SMRT_COLLECTION_BASE_NAMES, isSmrtCollectionExtendsName } from "./registry/collection-resolution.js";
19
19
  import { ObjectRegistry, smrt } from "./registry.js";
20
- import { DEFAULT_FACET_LIMIT, MAX_FACET_FIELDS, MAX_FACET_LIMIT, MAX_STI_READ_SCOPE_TYPES, SmrtCollection } from "./collection.js";
20
+ import { DEFAULT_FACET_LIMIT, EmbeddingUnavailableError, MAX_FACET_FIELDS, MAX_FACET_LIMIT, MAX_STI_READ_SCOPE_TYPES, SmrtCollection } from "./collection.js";
21
21
  import { SmrtHierarchical } from "./hierarchical.js";
22
22
  import { SmrtJunction } from "./junction.js";
23
23
  import { SmrtPolymorphicAssociation } from "./polymorphic-association.js";
@@ -27,4 +27,4 @@ import { BOOTED_PROVENANCE, assertPlainJson, sanitizeMessagePaths, snapshotRegis
27
27
  import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./system/retention.js";
28
28
  import "./system/index.js";
29
29
  import "./tools/index.js";
30
- export { AIError, AiUsageCollector, AiUsagePersistenceHandler, BOOTED_PROVENANCE, ConfigurationError, DEFAULT_AI_COST_RATES, DEFAULT_FACET_LIMIT, DEFAULT_RETENTION_POLICY, DIAGNOSTICS_DEFAULT_LIMIT, DatabaseError, ErrorUtils, FilesystemError, MAX_FACET_FIELDS, MAX_FACET_LIMIT, MAX_STI_READ_SCOPE_TYPES, MetricsAdapter, NetworkError, ObjectRegistry, PubSubAdapter, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SYSTEM_DIAGNOSTICS_TABLES, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, assertPlainJson, assertPostgresSystemTimestampsCurrent, classifyDatabaseError, classifyDialectMessage, clearRetentionTasks, config, convertTypeToJsonSchema, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, ensureSystemTables, estimateAiUsageCost, executeToolCall, executeToolCalls, formatToolResults, generateToolFromMethod, generateToolManifest, getDatabaseEngine, getRetentionTasks, isAbortedTransactionError, isDeterministicDatabaseError, isFrameworkBaseClass, isNotNullViolationError, isSmrtCollectionExtendsName, isTransientDatabaseError, isUniqueViolationError, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, pruneAiUsage, pruneExpiredContexts, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics, registerRetentionTask, runRetentionSweep, sanitizeMessagePaths, shouldIncludeMethod, smrt, smrt as smrtRegistry, snapshotRegistry, tableExists, unregisterRetentionTask, validateToolCall };
30
+ export { AIError, AiUsageCollector, AiUsagePersistenceHandler, BOOTED_PROVENANCE, ConfigurationError, DEFAULT_AI_COST_RATES, DEFAULT_FACET_LIMIT, DEFAULT_RETENTION_POLICY, DIAGNOSTICS_DEFAULT_LIMIT, DatabaseError, EmbeddingUnavailableError, ErrorUtils, FilesystemError, MAX_FACET_FIELDS, MAX_FACET_LIMIT, MAX_STI_READ_SCOPE_TYPES, MetricsAdapter, NetworkError, ObjectRegistry, PubSubAdapter, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SYSTEM_DIAGNOSTICS_TABLES, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, assertPlainJson, assertPostgresSystemTimestampsCurrent, classifyDatabaseError, classifyDialectMessage, clearRetentionTasks, config, convertTypeToJsonSchema, ensureBootstrapSystemTableCompatibility, ensureDeferredSystemTableCompatibility, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, ensureSystemTables, estimateAiUsageCost, executeToolCall, executeToolCalls, formatToolResults, generateToolFromMethod, generateToolManifest, getDatabaseEngine, getRetentionTasks, isAbortedTransactionError, isDeterministicDatabaseError, isFrameworkBaseClass, isNotNullViolationError, isSmrtCollectionExtendsName, isTransientDatabaseError, isUniqueViolationError, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, pruneAiUsage, pruneExpiredContexts, readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth, readSystemDiagnostics, registerRetentionTask, runRetentionSweep, sanitizeMessagePaths, shouldIncludeMethod, smrt, smrt as smrtRegistry, snapshotRegistry, tableExists, unregisterRetentionTask, validateToolCall };
@@ -1 +1 @@
1
- {"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAK5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAKpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAM1D;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAsB9C,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,8EAA8E;IAC9E,MAAM,EAAE,cAAc,CAAC;IACvB,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;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAE5E;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAC7B,SAAS,EAAE,MAAM,GAChB,WAAW,CAyIb;AAkZD,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;;;;;;;;;;;;;;GAcG;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,CAuDxB"}
1
+ {"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAK5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAKpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAM1D;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAsB9C,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,8EAA8E;IAC9E,MAAM,EAAE,cAAc,CAAC;IACvB,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;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,SAAS,CAE5E;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,mBAAmB,EAC7B,SAAS,EAAE,MAAM,GAChB,WAAW,CAyIb;AAqZD,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;;;;;;;;;;;;;;GAcG;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,CAuDxB"}
package/dist/cascade.js CHANGED
@@ -382,16 +382,19 @@ async function resolveReferences(ctx, className, tableName, ids, depth) {
382
382
  ctx.affectedTables.add(reference.tableName);
383
383
  ctx.affectedTableClasses.set(reference.tableName, toQualifiedClassName(ctx.registry, reference.className));
384
384
  }
385
- for (const association of plan.polymorphic) for (const batch of chunkArray(pending, 900)) {
386
- const where = { ...idPredicate("meta_id", batch) };
387
- if (plan.metaTypes.length === 1) where.meta_type = plan.metaTypes[0];
388
- else where["meta_type in"] = plan.metaTypes;
389
- if (((await tolerateMissingTable(() => ctx.db.delete(association.tableName, where), void 0, {
390
- table: association.tableName,
391
- action: "polymorphic association cleanup"
392
- }))?.affected ?? 0) > 0) {
393
- ctx.affectedTables.add(association.tableName);
394
- ctx.affectedTableClasses.set(association.tableName, toQualifiedClassName(ctx.registry, association.className));
385
+ for (const association of plan.polymorphic) {
386
+ if (!await ctx.db.tableExists(association.tableName)) continue;
387
+ for (const batch of chunkArray(pending, 900)) {
388
+ const where = { ...idPredicate("meta_id", batch) };
389
+ if (plan.metaTypes.length === 1) where.meta_type = plan.metaTypes[0];
390
+ else where["meta_type in"] = plan.metaTypes;
391
+ if (((await tolerateMissingTable(() => ctx.db.delete(association.tableName, where), void 0, {
392
+ table: association.tableName,
393
+ action: "polymorphic association cleanup"
394
+ }))?.affected ?? 0) > 0) {
395
+ ctx.affectedTables.add(association.tableName);
396
+ ctx.affectedTableClasses.set(association.tableName, toQualifiedClassName(ctx.registry, association.className));
397
+ }
395
398
  }
396
399
  }
397
400
  }