@happyvertical/smrt-core 0.42.6 → 0.43.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 +39 -43
- package/agents/collection-reads.md +40 -0
- package/agents/data-query.md +21 -0
- package/agents/latest-related.md +35 -0
- package/agents/memory.md +16 -0
- package/agents/schema-paths.md +29 -15
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts +7 -6
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +19 -21
- package/dist/cascade.js.map +1 -1
- package/dist/class.d.ts +17 -0
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +30 -0
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +164 -3
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +345 -10
- package/dist/collection.js.map +1 -1
- package/dist/decorators/index.d.ts +33 -15
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +16 -11
- package/dist/decorators/index.js.map +1 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +10 -2
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/tool-schema.d.ts +14 -0
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +13 -1
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/interceptors.d.ts +2 -1
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +53 -1
- 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 +62 -1
- package/dist/migrations/differ.d.ts +2 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +75 -0
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +19 -2
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +4 -1
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/object.d.ts +4 -4
- package/dist/object.js +4 -4
- package/dist/object.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +9 -0
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/schema-builder.d.ts +6 -0
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +78 -8
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry.d.ts +5 -7
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -8
- package/dist/registry.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +62 -18
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +5 -1
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -0
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.d.ts +1 -0
- package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/duckdb-strategy.js +17 -1
- package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +9 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -0
- package/dist/schema/foreign-key-ddl.js +53 -0
- package/dist/schema/foreign-key-ddl.js.map +1 -0
- package/dist/schema/foreign-key-planner.d.ts +18 -0
- package/dist/schema/foreign-key-planner.d.ts.map +1 -0
- package/dist/schema/foreign-key-planner.js +109 -0
- package/dist/schema/foreign-key-planner.js.map +1 -0
- package/dist/schema/foreign-key-policy.d.ts +21 -0
- package/dist/schema/foreign-key-policy.d.ts.map +1 -0
- package/dist/schema/foreign-key-policy.js +41 -0
- package/dist/schema/foreign-key-policy.js.map +1 -0
- package/dist/schema/generator.d.ts +3 -0
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +54 -10
- package/dist/schema/generator.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/manifest-schema.d.ts.map +1 -1
- package/dist/schema/manifest-schema.js +20 -3
- package/dist/schema/manifest-schema.js.map +1 -1
- package/dist/schema/schema-aggregator.d.ts +7 -1
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +54 -9
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/schema-manager.d.ts +1 -6
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +26 -30
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +8 -5
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +14 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +63 -8
- package/dist/testing/database.d.ts +11 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +80 -9
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +12 -3
- 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 +24 -3
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +3 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +24 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +4 -4
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,8 @@ subsystem you are editing. This file keeps what holds across all of them.
|
|
|
18
18
|
| `src/change-signals.ts` + the generated `_events` SSE route | the push companion to the change feed — the signal bus, cross-replica fan-out, the SSE route, and its documented gaps | [agents/change-signals.md](agents/change-signals.md) |
|
|
19
19
|
| `src/generators/` + `src/vite-plugin/web-collections.ts` | REST/CLI/MCP/web-collection generation, the `manifestHash` emission sites, and generated conditional-GET / ETag v2 semantics | [agents/generators.md](agents/generators.md) |
|
|
20
20
|
| `src/schema/` | the four `SchemaGenerator` entry points, which two reach production, why schema drift stayed invisible, and the #2382 index/tenancy rules | [agents/schema-paths.md](agents/schema-paths.md) |
|
|
21
|
-
| `src/data-query.ts` | canonical bounded data-query normalizer
|
|
21
|
+
| `src/data-query.ts` | canonical bounded data-query normalizer and transport-neutral envelope (#2444) | [agents/data-query.md](agents/data-query.md) |
|
|
22
|
+
| `src/collection.ts` | bounded collection reads, projections, latest-related hydration, facets, counts, and read plans | [agents/collection-reads.md](agents/collection-reads.md) |
|
|
22
23
|
|
|
23
24
|
## SmrtObject Lifecycle
|
|
24
25
|
|
|
@@ -37,7 +38,8 @@ subsystem you are editing. This file keeps what holds across all of them.
|
|
|
37
38
|
`_smrt_contexts` plus optional injected semantic search. `capture()` reinforces
|
|
38
39
|
successes and decays failures while updating outcome counters; `recall()`
|
|
39
40
|
applies confidence, expiry, time-decay, and hierarchical-scope filters and
|
|
40
|
-
refreshes `last_used_at`.
|
|
41
|
+
refreshes `last_used_at`. Detailed persistence and search semantics are in
|
|
42
|
+
[agents/memory.md](agents/memory.md). Keep semantic search behind the
|
|
41
43
|
`SmrtCollection.semanticSearch`-compatible injection boundary.
|
|
42
44
|
|
|
43
45
|
## SmrtCollection Query
|
|
@@ -49,13 +51,8 @@ await collection.list({
|
|
|
49
51
|
});
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
Projection
|
|
53
|
-
|
|
54
|
-
field names, maps them to DB columns internally, and returns plain objects keyed
|
|
55
|
-
by the same SMRT field names without hydrating `SmrtObject` instances. It
|
|
56
|
-
composes with `where`, `orderBy`, `limit`, and `offset`; `beforeList`
|
|
57
|
-
interceptors still run. It is for column-backed fields only and cannot combine
|
|
58
|
-
with `include`/relationship eager loading.
|
|
54
|
+
Projection, latest-related, facets, counts, and bounded read plans are
|
|
55
|
+
documented in [agents/collection-reads.md](agents/collection-reads.md).
|
|
59
56
|
|
|
60
57
|
`list()` and `query()` hydrate model instances serially in result order because
|
|
61
58
|
an `initialize()` hook may query through the same transaction-bound PostgreSQL
|
|
@@ -79,43 +76,18 @@ operator against a database to keep the two in step.
|
|
|
79
76
|
|
|
80
77
|
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 [agents/query-bounds.md](agents/query-bounds.md).
|
|
81
78
|
|
|
82
|
-
## Bounded Collection Read Plans
|
|
83
|
-
|
|
84
|
-
Use `executeCollectionReadPlan()` when one operation needs several independent
|
|
85
|
-
collections. It bounds top-level `collection.list()` concurrency while keeping
|
|
86
|
-
all reads on the normal registry/collection path. Callers must choose an
|
|
87
|
-
explicit positive `maxConcurrency` and pass their normal shared
|
|
88
|
-
`collectionOptions` when database or tenant context matters.
|
|
89
|
-
|
|
90
|
-
The executor deliberately does not compose SQL, cache the plan, or change pool
|
|
91
|
-
defaults. On failure it stops starting queued entries, drains operations already
|
|
92
|
-
in flight, and rethrows the first error.
|
|
93
|
-
|
|
94
79
|
## Canonical Bounded Data Queries (#2444)
|
|
95
80
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
never execute a query or decide tenant/principal access.
|
|
101
|
-
|
|
102
|
-
Use `createDataQueryFingerprint()` for cache and result correlation. It omits
|
|
103
|
-
the request id and page position, canonicalizes equivalent filter/projection/
|
|
104
|
-
facet forms, and adds the identity sort tie-break. Keep data-query values
|
|
105
|
-
scalar, requests/pages/facets positive and bounded, results within the schema
|
|
106
|
-
byte cap with declared field types preserved. Datetimes must be valid RFC 3339
|
|
107
|
-
instants, identity fields must be string/number/datetime-compatible, and JSON
|
|
108
|
-
result fields are depth/container/string/byte bounded before cloning. Return
|
|
109
|
-
only normalized `DataQueryResult` envelopes to REST, MCP,
|
|
110
|
-
WebMCP, and browser consumers. Adapter-specific report/content context wraps
|
|
111
|
-
the base envelope; it does not add unsafe fields or SQL-like controls to it.
|
|
81
|
+
The normalizers and fingerprint are the trust boundary for the
|
|
82
|
+
transport-neutral query envelope; full bounds, schema, and output rules live in
|
|
83
|
+
[agents/data-query.md](agents/data-query.md). Adapters own tenant/principal
|
|
84
|
+
access and query execution.
|
|
112
85
|
|
|
113
86
|
## Object Memory & Semantic Search
|
|
114
87
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- **Semantic search** (on `SmrtCollection`, table `_smrt_embeddings`): `semanticSearch(query)`, `findSimilar(object)`, `findSimilarToEmbedding(vector)` — cosine ranking over embeddings of the fields declared in `@smrt({ embeddings })`. Native pgvector/HNSW when configured, in-memory `CosineSimilarity` fallback otherwise; default local model `Xenova/bge-base-en-v1.5` (768-dim) or AI `text-embedding-3-small`. Hits hydrate via `list({ 'id in': … })`, so `@TenantScoped` isolation applies to results.
|
|
88
|
+
Context memory and semantic search are persistence primitives inherited by
|
|
89
|
+
`SmrtObject`/`SmrtCollection`; their storage, scope, expiry, and tenant
|
|
90
|
+
invariants are in [agents/memory.md](agents/memory.md).
|
|
119
91
|
|
|
120
92
|
## @smrt() Decorator Options
|
|
121
93
|
|
|
@@ -235,12 +207,35 @@ bootstrap prevents missing-table probes from poisoning that transaction.
|
|
|
235
207
|
Production DDL comes from the **manifest** paths
|
|
236
208
|
(`generateSTISchemaFromManifest`/`generateCTISchemaFromManifest`, selected in
|
|
237
209
|
`src/scanner/manifest-generator.ts` → registered `schema` → `db:migrate`). The
|
|
238
|
-
**registry** paths feed `getTestDatabase()
|
|
239
|
-
|
|
210
|
+
**registry** paths feed `getTestDatabase()`. Manifest and registry schemas must
|
|
211
|
+
agree on same-package foreign keys as well as columns and indexes:
|
|
212
|
+
`@foreignKey` emits a named physical constraint, while `@crossPackageRef` and
|
|
213
|
+
`@tenantId` remain indexed runtime relationships without physical constraints.
|
|
214
|
+
Natural-key references default to `CASCADE`; ordinary references default to
|
|
215
|
+
immediate `NO ACTION`, matching `SmrtObject.delete()`.
|
|
216
|
+
|
|
217
|
+
Same-package archival/audit identifiers that intentionally outlive their
|
|
218
|
+
parent may use `@foreignKey(Target, { constraint: false })`. This explicit
|
|
219
|
+
exception retains relationship loading, indexing, and application-side delete
|
|
220
|
+
metadata while omitting the physical constraint, schema dependency, and
|
|
221
|
+
app-side cascade/preflight action so the stored identifier survives deletion;
|
|
222
|
+
document the retention reason at the field, and keep ordinary same-package
|
|
223
|
+
relationships constrained.
|
|
240
224
|
|
|
241
225
|
- Change column/index emission on every shipping path, proven by the path-parity
|
|
242
226
|
test `src/schema/schema-path-parity.test.ts` (#2359; index rules in the module doc). A "same as migrations" comment is a claim to check.
|
|
243
227
|
- Every new query predicate ships with its index, or a reason it doesn't.
|
|
228
|
+
- Creation is dependency-planned on every entry point. PostgreSQL defers mutual
|
|
229
|
+
cycle constraints until both tables exist; SQLite keeps cycles inline;
|
|
230
|
+
DuckDB refuses unsupported cycles/actions rather than silently omitting them.
|
|
231
|
+
In particular, generated same-package constraints retain the compatibility
|
|
232
|
+
default `ON UPDATE CASCADE`; DuckDB/JSON cannot enforce that action and must
|
|
233
|
+
return an actionable refusal instead of stripping the clause.
|
|
234
|
+
PostgreSQL deferred adds are idempotent and probe the exact child/parent
|
|
235
|
+
columns for orphans before `NOT VALID` + validation. Rollback drops children
|
|
236
|
+
before parents, removes deferred PostgreSQL cycle constraints first, and
|
|
237
|
+
defers SQLite checks while dropping populated cycles. Schema aggregation that
|
|
238
|
+
deliberately filters a parent also removes the retained child's physical FK.
|
|
244
239
|
- Numeric types, uuid casts, conflict targets, timestamps, migrations: run the
|
|
245
240
|
`test:postgres` lane — SQLite affinity accepts what PostgreSQL rejects.
|
|
246
241
|
- Read `dist/manifest.json`/regenerated schemas for what a decorator produced;
|
|
@@ -255,6 +250,7 @@ production never gets: the suite runs on a richer schema than it ships.
|
|
|
255
250
|
## Gotchas
|
|
256
251
|
|
|
257
252
|
- **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.
|
|
253
|
+
- **Transaction-bound instances**: `SmrtClass.withDatabase(db, callback)` temporarily binds an initialized instance (including its public `options.db`) to a supplied transaction database and restores the original binding on success or failure. Use it when domain validation and persistence must share one transaction; never reach into `_db`, and do not use the same instance concurrently during the callback.
|
|
258
254
|
- **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`
|
|
259
255
|
- **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)
|
|
260
256
|
- **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime verification is `tableExists()` only (`src/schema/table-verifier.ts`) — no column, type, or index check
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->
|
|
2
|
+
|
|
3
|
+
# Collection reads
|
|
4
|
+
|
|
5
|
+
This module covers bounded collection reads beyond the basic query contract in
|
|
6
|
+
`packages/core/AGENTS.md`.
|
|
7
|
+
|
|
8
|
+
## Projections and related rows
|
|
9
|
+
|
|
10
|
+
`list({ select })` uses SMRT field names, maps them to database columns, and
|
|
11
|
+
returns plain rows without hydrating objects. It composes with `where`,
|
|
12
|
+
`orderBy`, `limit`, and `offset`, runs normal `beforeList`/tenant interceptors,
|
|
13
|
+
and is limited to column-backed fields; it cannot combine with `include`.
|
|
14
|
+
|
|
15
|
+
For one child per parent, use
|
|
16
|
+
[`latest-related.md`](latest-related.md). It uses a portable ranked CTE,
|
|
17
|
+
declared primary keys, adapter-specific offset-only syntax, explicit aliases,
|
|
18
|
+
and hydrates only the visible parent page.
|
|
19
|
+
|
|
20
|
+
## Facets, counts, and read plans
|
|
21
|
+
|
|
22
|
+
`collection.facets({ fields, where })` runs one bounded `GROUP BY` per requested
|
|
23
|
+
field and returns `{ field, values: [{ value, count }] }`. It accepts at most 20
|
|
24
|
+
fields, clamps value limits to 1,000 and the collection ceiling, never hydrates
|
|
25
|
+
objects, and applies the same read/tenant/sensitive-field rails as `select`.
|
|
26
|
+
Stored array/string-list values are grouped as stored; they are not unnested.
|
|
27
|
+
`collection.counts({ where })` returns `{ total, filtered }` through two scoped
|
|
28
|
+
`COUNT(*)` queries. Local coverage is SQLite/DuckDB; optional scalar PostgreSQL
|
|
29
|
+
coverage requires `SMRT_TEST_POSTGRES_URL`.
|
|
30
|
+
|
|
31
|
+
`executeCollectionReadPlan()` bounds concurrent reads across independent
|
|
32
|
+
collections while preserving the normal registry and collection options. The
|
|
33
|
+
caller supplies a positive `maxConcurrency`; the executor does not compose SQL,
|
|
34
|
+
cache, or alter pool defaults, and drains already-started work before returning
|
|
35
|
+
the first error.
|
|
36
|
+
|
|
37
|
+
`where` operators must remain aligned with `@happyvertical/sql`'s `buildWhere`:
|
|
38
|
+
`=`, `>`, `<`, `>=`, `<=`, `!=`, `in`, `not in`, and `like`. Arrays imply `IN`,
|
|
39
|
+
and null values render `IS NULL`/`IS NOT NULL`. `contains` and dot-notation JSON
|
|
40
|
+
paths are intentionally rejected until the SQL layer supports them.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->
|
|
2
|
+
|
|
3
|
+
# Bounded data queries (#2444)
|
|
4
|
+
|
|
5
|
+
`normalizeDataQueryRequest()` and `normalizeDataQueryResult()` define the trust
|
|
6
|
+
boundary for the transport-neutral table/report/content query envelope. A
|
|
7
|
+
trusted adapter supplies `DataQuerySchema`; callers receive only its declared
|
|
8
|
+
projectable, sortable, filterable, and facetable fields. The helpers validate
|
|
9
|
+
and normalize but never execute SQL or decide tenant/principal access.
|
|
10
|
+
|
|
11
|
+
Use `createDataQueryFingerprint()` for cache and result correlation. It omits
|
|
12
|
+
request id and page position, canonicalizes equivalent filter/projection/facet
|
|
13
|
+
forms, and adds the identity sort tie-break. Keep values scalar and request,
|
|
14
|
+
page, and facet sizes positive and bounded. Results must stay within the schema
|
|
15
|
+
byte cap with declared field types preserved; datetimes are RFC 3339 instants,
|
|
16
|
+
identity fields are string/number/datetime-compatible, and JSON values are
|
|
17
|
+
bounded by depth, container count, string size, and bytes before cloning.
|
|
18
|
+
|
|
19
|
+
Only normalized `DataQueryResult` envelopes cross REST, MCP, WebMCP, and browser
|
|
20
|
+
boundaries. Adapter-specific report/content context wraps the base envelope; it
|
|
21
|
+
does not add unsafe fields or SQL-like controls.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->
|
|
2
|
+
|
|
3
|
+
# Latest-related reads (#1903)
|
|
4
|
+
|
|
5
|
+
Use `SmrtCollection.listWithLatestRelated()` when a page needs one row from a
|
|
6
|
+
declared `@oneToMany` relationship without N+1 queries or hydrating unrelated
|
|
7
|
+
children. `latestRelated.orderBy` ranks rows within each parent, `select`
|
|
8
|
+
chooses the returned child fields (defaulting to the related model's declared
|
|
9
|
+
primary key), and an optional `sortBy` orders parents by the winning child
|
|
10
|
+
before the parent `limit`/`offset` are applied. The result is
|
|
11
|
+
`{ parent, latestRelated }`: `parent` is hydrated and `latestRelated` is a
|
|
12
|
+
plain projection or `null` when the parent has no child.
|
|
13
|
+
|
|
14
|
+
The read plan uses a portable `ROW_NUMBER()` CTE with explicit null-last
|
|
15
|
+
ordering and the declared primary keys for every tie-break, join, and result
|
|
16
|
+
map. The parent primary key is never assumed to be `id`; the related primary
|
|
17
|
+
key is the default projection when `select` is omitted. Only the visible
|
|
18
|
+
parent page is hydrated.
|
|
19
|
+
|
|
20
|
+
Pagination is adapter-aware: SQLite uses `LIMIT -1` for offset-only reads,
|
|
21
|
+
while DuckDB and PostgreSQL use `LIMIT ALL`. In-memory DuckDB and JSON adapters
|
|
22
|
+
retain their engine hint even when no URL or database type is present, so the
|
|
23
|
+
generated syntax remains legal for the actual driver.
|
|
24
|
+
|
|
25
|
+
Internal result aliases are bounded `__smrt_lr_N` identifiers. The allocator
|
|
26
|
+
reserves declared and live parent table columns, and parent columns are
|
|
27
|
+
projected explicitly (using the public table-schema API where available) rather
|
|
28
|
+
than `parent.*`, so externally added columns cannot collide or corrupt the
|
|
29
|
+
latest-related projection. JSON adapters without live schema introspection use
|
|
30
|
+
the declared schema as their fallback.
|
|
31
|
+
|
|
32
|
+
The primitive preserves normal read interceptors, tenant and STI scope, and
|
|
33
|
+
does not expose a cache option until cache invalidation is implemented. Tests
|
|
34
|
+
cover custom primary keys, SQLite/DuckDB offset-only pagination, long field
|
|
35
|
+
names, external alias collisions, and cleanup of temporary database files.
|
package/agents/memory.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->
|
|
2
|
+
|
|
3
|
+
# Object memory and semantic search
|
|
4
|
+
|
|
5
|
+
Context memory (`remember`, `recall`, `recallAll`, `forget`, `forgetScope`) is
|
|
6
|
+
stored in `_smrt_contexts`, keyed by owner, scope, key, and version. Values have
|
|
7
|
+
a 0–1 confidence and optional expiry metadata; `recall()` does not filter
|
|
8
|
+
expired rows. Ancestor fallback is opt-in (`includeAncestors: true`) and walks
|
|
9
|
+
`a/b/c → a/b → a → global`. `LearningMemory` owns outcome counters and expiry
|
|
10
|
+
filtering; object/collection recall does not update them.
|
|
11
|
+
|
|
12
|
+
Semantic search uses `_smrt_embeddings` and cosine ranking over fields declared
|
|
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.
|
package/agents/schema-paths.md
CHANGED
|
@@ -648,30 +648,26 @@ upsert conflict target. When reading a PostgreSQL catalog array, cast it
|
|
|
648
648
|
the array OID returns the raw `{a,b}` literal, and reading that as "no columns"
|
|
649
649
|
silently inverts an index-existence decision.
|
|
650
650
|
|
|
651
|
-
##
|
|
651
|
+
## Same-package referential integrity uses two matching rails
|
|
652
652
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
migrate paths (neither orders today), and needs a plan for the orphans already in
|
|
658
|
-
production databases. Emitting them is tracked separately.
|
|
659
|
-
|
|
660
|
-
What `@foreignKey(..., { onDelete })` therefore means is *application* behaviour,
|
|
661
|
-
applied by `SmrtObject.delete()` through `src/cascade.ts` (#2371):
|
|
653
|
+
`@foreignKey(Target)` emits a physical database constraint when the target is
|
|
654
|
+
in the same package and applies the same action through `SmrtObject.delete()`
|
|
655
|
+
in `src/cascade.ts`. A shared delete-action resolver keeps both paths aligned;
|
|
656
|
+
the established generated `ON UPDATE CASCADE` default remains unchanged:
|
|
662
657
|
|
|
663
658
|
| Reference | Default when `onDelete` is absent |
|
|
664
659
|
|---|---|
|
|
665
660
|
| Column is part of the referencing class's `conflictColumns`, and is not a `@tenantId()` field | `CASCADE` |
|
|
666
661
|
| Polymorphic `(metaType, metaId)` association row | `CASCADE` |
|
|
667
|
-
|
|
|
662
|
+
| Ordinary same-package reference | `NO ACTION` — deletion is refused while references remain |
|
|
663
|
+
| Every `@tenantId()` field | Excluded from physical constraints and delete cascades |
|
|
668
664
|
|
|
669
665
|
The natural-key rule is what cleans junction rows up without any per-package
|
|
670
666
|
annotation: a junction declares
|
|
671
667
|
`@smrt({ conflictColumns: ['content_id', 'asset_id', 'relationship'] })`, so the
|
|
672
668
|
row is *identified* by the content and cannot outlive it. An ordinary child
|
|
673
|
-
(`Order.customerId`) is keyed by `(slug, context)` and
|
|
674
|
-
|
|
669
|
+
(`Order.customerId`) is keyed by `(slug, context)` and therefore defaults to
|
|
670
|
+
immediate `NO ACTION` unless it opts in explicitly.
|
|
675
671
|
|
|
676
672
|
**`@tenantId()` is excluded even though it lands in `conflictColumns`.**
|
|
677
673
|
#2360 leads every tenant-scoped class's *default* natural key with the
|
|
@@ -682,8 +678,26 @@ tenant column scopes ownership; it does not identify the row the way a
|
|
|
682
678
|
junction's foreign key does. Detected via the `__tenancy.isTenantIdField`
|
|
683
679
|
marker on `FieldMeta` (`smrt-core` reads it structurally so it never depends
|
|
684
680
|
on `smrt-tenancy`). `@tenantId()` exposes no `onDelete` option today, so
|
|
685
|
-
this cannot currently be overridden per field
|
|
686
|
-
|
|
681
|
+
this cannot currently be overridden per field.
|
|
682
|
+
|
|
683
|
+
`@crossPackageRef()` remains runtime-only: it registers relationship loading
|
|
684
|
+
and indexes but deliberately emits no physical constraint, avoiding circular
|
|
685
|
+
package DDL. Tenant markers follow the same non-constraint rule because a
|
|
686
|
+
tenant is a scope, not an ownership edge.
|
|
687
|
+
|
|
688
|
+
Every schema creation entry point uses the same deterministic dependency
|
|
689
|
+
planner. Parents are created before children. SQLite keeps cycle constraints
|
|
690
|
+
inline because it can create them safely. PostgreSQL creates mutually dependent
|
|
691
|
+
tables first and adds their named constraints afterward. DuckDB refuses cycles,
|
|
692
|
+
self-references, `CASCADE`, and `SET NULL` with an actionable error because its
|
|
693
|
+
current ALTER/constraint support cannot enforce those shapes safely.
|
|
694
|
+
|
|
695
|
+
For existing tables, PostgreSQL checks the exact child table/column against the
|
|
696
|
+
exact referenced table/column before adding a constraint as `NOT VALID` and
|
|
697
|
+
then validating it. If orphans exist (or the probe cannot run), migration stops
|
|
698
|
+
with detector and repair SQL. SQLite requires a deliberate table rebuild;
|
|
699
|
+
DuckDB reports the unsupported ALTER path. Neither engine treats an unsupported
|
|
700
|
+
constraint addition as a successful no-op.
|
|
687
701
|
|
|
688
702
|
Properties to keep if you touch that module:
|
|
689
703
|
|
package/dist/browser.js
CHANGED
|
@@ -16,7 +16,7 @@ import { executeToolCall, executeToolCalls, formatToolResults, validateToolCall
|
|
|
16
16
|
import { SmrtObject } from "./object.js";
|
|
17
17
|
import { SMRT_COLLECTION_BASE_NAMES, isSmrtCollectionExtendsName } from "./registry/collection-resolution.js";
|
|
18
18
|
import { ObjectRegistry, smrt } from "./registry.js";
|
|
19
|
-
import { SmrtCollection } from "./collection.js";
|
|
19
|
+
import { DEFAULT_FACET_LIMIT, MAX_FACET_FIELDS, MAX_FACET_LIMIT, SmrtCollection } from "./collection.js";
|
|
20
20
|
import { SmrtHierarchical } from "./hierarchical.js";
|
|
21
21
|
import { SmrtJunction } from "./junction.js";
|
|
22
22
|
import { SmrtPolymorphicAssociation } from "./polymorphic-association.js";
|
|
@@ -24,4 +24,4 @@ import "./signals/index.js";
|
|
|
24
24
|
import { DEFAULT_RETENTION_POLICY, clearRetentionTasks, getRetentionTasks, pruneAiUsage, pruneExpiredContexts, registerRetentionTask, runRetentionSweep, unregisterRetentionTask } from "./system/retention.js";
|
|
25
25
|
import "./system/index.js";
|
|
26
26
|
import "./tools/index.js";
|
|
27
|
-
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, ensureSystemTables, 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 };
|
|
27
|
+
export { AIError, AiUsageCollector, AiUsagePersistenceHandler, ConfigurationError, DEFAULT_AI_COST_RATES, DEFAULT_FACET_LIMIT, DEFAULT_RETENTION_POLICY, DatabaseError, ErrorUtils, FilesystemError, MAX_FACET_FIELDS, MAX_FACET_LIMIT, 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, ensureSystemTables, 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 };
|
package/dist/cascade.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { DatabaseInterface } from '@happyvertical/sql';
|
|
2
2
|
import { ObjectRegistry } from './registry.js';
|
|
3
|
+
import { ForeignKeyAction } from './schema/types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Referential action applied to rows pointing at a deleted object.
|
|
5
6
|
*
|
|
6
|
-
* Same vocabulary as SQL's `ON DELETE`, enforced by the framework
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Same vocabulary as SQL's `ON DELETE`, enforced by the framework before the
|
|
8
|
+
* engine. `NO ACTION` is preflighted like an immediate restrictive action so
|
|
9
|
+
* application-side and database-side enforcement agree.
|
|
9
10
|
*/
|
|
10
|
-
export type OnDeleteAction =
|
|
11
|
+
export type OnDeleteAction = ForeignKeyAction;
|
|
11
12
|
/** One reference that must be resolved before the target row can be removed. */
|
|
12
13
|
export interface CascadeReference {
|
|
13
14
|
/** Registered name of the class holding the referencing column. */
|
|
@@ -18,8 +19,8 @@ export interface CascadeReference {
|
|
|
18
19
|
fieldName: string;
|
|
19
20
|
/** Column (snake_case) holding the reference. */
|
|
20
21
|
column: string;
|
|
21
|
-
/** Resolved action
|
|
22
|
-
action:
|
|
22
|
+
/** Resolved action. `NO ACTION` is preflighted like an immediate RESTRICT. */
|
|
23
|
+
action: OnDeleteAction;
|
|
23
24
|
/** `true` when the action was declared rather than derived from the key. */
|
|
24
25
|
declared: boolean;
|
|
25
26
|
}
|
package/dist/cascade.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cascade.d.ts","sourceRoot":"","sources":["../src/cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;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,CAgIb;AAkVD,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
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { classifyDatabaseError } from "./db-errors.js";
|
|
2
2
|
import { ConfigurationError, DatabaseError } from "./errors.js";
|
|
3
|
+
import { normalizeForeignKeyAction, resolveForeignKeyDeleteAction } from "./schema/foreign-key-policy.js";
|
|
3
4
|
import { chunkArray } from "./utils/chunk.js";
|
|
4
5
|
import { toSnakeCase } from "./utils/naming.js";
|
|
5
6
|
import { createLogger } from "@happyvertical/logger";
|
|
@@ -7,8 +8,9 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
7
8
|
/**
|
|
8
9
|
* App-side referential integrity for `SmrtObject.delete()` (#2371).
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Same-package `@foreignKey` fields now emit database constraints where the
|
|
12
|
+
* engine can enforce the declared policy. Cross-package references and older
|
|
13
|
+
* schemas still rely on this application-side path. Before this module,
|
|
12
14
|
* `delete()` removed the object's own row and nothing else: junction rows,
|
|
13
15
|
* polymorphic association rows, `_smrt_embeddings` and `_smrt_contexts` entries
|
|
14
16
|
* were all left pointing at an id that no longer resolved, and
|
|
@@ -45,14 +47,15 @@ import { createLogger } from "@happyvertical/logger";
|
|
|
45
47
|
* |---|---|
|
|
46
48
|
* | Column is part of the referencing class's `conflictColumns`, and is not a `@tenantId()` field | `CASCADE` |
|
|
47
49
|
* | Polymorphic `(metaType, metaId)` association row | `CASCADE` |
|
|
48
|
-
* |
|
|
50
|
+
* | Ordinary same-package reference | `NO ACTION` (deletion is refused while references remain) |
|
|
51
|
+
* | Every `@tenantId()` field | Excluded: tenant scope is not an ownership edge |
|
|
49
52
|
*
|
|
50
53
|
* The natural-key rule is what makes junction rows work without any
|
|
51
54
|
* per-package annotation: a junction declares
|
|
52
55
|
* `@smrt({ conflictColumns: ['content_id', 'asset_id', 'relationship'] })`, so
|
|
53
56
|
* `content_id` identifies the row and the row cannot outlive the content it
|
|
54
57
|
* links. An ordinary child (`Order.customerId`) is keyed by `(slug, context)`,
|
|
55
|
-
* so it
|
|
58
|
+
* so it defaults to immediate `NO ACTION` unless it opts in with
|
|
56
59
|
* `@foreignKey(Customer, { onDelete: 'CASCADE' })`.
|
|
57
60
|
*
|
|
58
61
|
* `@tenantId()` fields are excluded from the natural-key rule even though
|
|
@@ -88,12 +91,6 @@ var COLLECTION_OWNER_SENTINEL = "__collection__";
|
|
|
88
91
|
* is broken by the visited set; this bound catches unbounded *chains*.
|
|
89
92
|
*/
|
|
90
93
|
var MAX_CASCADE_DEPTH = 10;
|
|
91
|
-
var CASCADE_ACTIONS = /* @__PURE__ */ new Set([
|
|
92
|
-
"CASCADE",
|
|
93
|
-
"SET NULL",
|
|
94
|
-
"RESTRICT",
|
|
95
|
-
"NO ACTION"
|
|
96
|
-
]);
|
|
97
94
|
/**
|
|
98
95
|
* Normalize a declared `onDelete` value.
|
|
99
96
|
*
|
|
@@ -102,9 +99,7 @@ var CASCADE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
102
99
|
* an unset or unrecognized value so the caller can fall back to the default.
|
|
103
100
|
*/
|
|
104
101
|
function normalizeOnDelete(value) {
|
|
105
|
-
|
|
106
|
-
const normalized = value.trim().toUpperCase().replace(/_/g, " ");
|
|
107
|
-
return CASCADE_ACTIONS.has(normalized) ? normalized : void 0;
|
|
102
|
+
return normalizeForeignKeyAction(value);
|
|
108
103
|
}
|
|
109
104
|
function isPolymorphicAssociationClass(fields) {
|
|
110
105
|
return fields.has("metaType") && fields.has("metaId") && fields.has("role");
|
|
@@ -142,14 +137,17 @@ function buildCascadePlan(registry, className) {
|
|
|
142
137
|
for (const relationship of relationships) {
|
|
143
138
|
if (relationship.type !== "foreignKey" && relationship.type !== "crossPackageRef") continue;
|
|
144
139
|
if (!targetNames.has(relationship.targetClass)) continue;
|
|
140
|
+
if (relationship.type === "foreignKey" && relationship.options?.constraint === false) continue;
|
|
145
141
|
const tableName = registry.getTableName(sourceClass);
|
|
146
142
|
if (!tableName) continue;
|
|
147
143
|
const column = toSnakeCase(relationship.fieldName);
|
|
148
|
-
const declaredAction = normalizeOnDelete(relationship.options?.onDelete);
|
|
149
144
|
if (!conflictColumns) conflictColumns = new Set(registry.getConflictColumns(sourceClass));
|
|
150
|
-
|
|
151
|
-
const action
|
|
152
|
-
|
|
145
|
+
if (relationship.options?.__tenancy?.isTenantIdField === true) continue;
|
|
146
|
+
const { action, declared } = resolveForeignKeyDeleteAction({
|
|
147
|
+
declared: relationship.options?.onDelete,
|
|
148
|
+
isConflictColumn: conflictColumns.has(column),
|
|
149
|
+
isTenantIdField: false
|
|
150
|
+
});
|
|
153
151
|
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
152
|
references.push({
|
|
155
153
|
className: sourceClass,
|
|
@@ -157,7 +155,7 @@ function buildCascadePlan(registry, className) {
|
|
|
157
155
|
fieldName: relationship.fieldName,
|
|
158
156
|
column,
|
|
159
157
|
action,
|
|
160
|
-
declared
|
|
158
|
+
declared
|
|
161
159
|
});
|
|
162
160
|
}
|
|
163
161
|
}
|
|
@@ -317,16 +315,16 @@ async function resolveReferences(ctx, className, tableName, ids, depth) {
|
|
|
317
315
|
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
316
|
const plan = buildCascadePlan(ctx.registry, className);
|
|
319
317
|
for (const reference of plan.references) {
|
|
320
|
-
if (reference.action !== "RESTRICT") continue;
|
|
318
|
+
if (reference.action !== "RESTRICT" && reference.action !== "NO ACTION") continue;
|
|
321
319
|
let remaining = 0;
|
|
322
320
|
for (const batch of chunkArray(pending, 900)) {
|
|
323
321
|
remaining += await tolerateMissingTable(() => ctx.db.count(reference.tableName, idPredicate(reference.column, batch)), 0, {
|
|
324
322
|
table: reference.tableName,
|
|
325
|
-
action:
|
|
323
|
+
action: `${reference.action} check`
|
|
326
324
|
});
|
|
327
325
|
if (remaining > 0) break;
|
|
328
326
|
}
|
|
329
|
-
if (remaining > 0) throw DatabaseError.constraintViolation(`${reference.className}.${reference.fieldName} declares onDelete: '
|
|
327
|
+
if (remaining > 0) throw DatabaseError.constraintViolation(`${reference.className}.${reference.fieldName} ${reference.declared ? "declares" : "resolves to"} onDelete: '${reference.action}' and ${remaining} row(s) still reference this ${className}`, reference.column);
|
|
330
328
|
}
|
|
331
329
|
for (const reference of plan.references) {
|
|
332
330
|
if (reference.action === "SET NULL") {
|