@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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ValidationError } from "./errors.js";
|
|
2
|
+
//#region src/query-bounds.ts
|
|
3
|
+
/**
|
|
4
|
+
* Query bounds for list surfaces (#2367).
|
|
5
|
+
*
|
|
6
|
+
* Every generated read surface — REST, SvelteKit routes, the MCP server and the
|
|
7
|
+
* MCP runtime bootstrap — takes `limit`/`offset` straight from untrusted input.
|
|
8
|
+
* Before this module each one parsed them by hand, and each got it wrong in a
|
|
9
|
+
* different way: `Number.parseInt('abc')` produced `LIMIT NaN` (a 500 from the
|
|
10
|
+
* driver, not a 400 from the API), `Number(x) || 50` silently turned `0` into
|
|
11
|
+
* `50`, and none of them capped the value, so `?limit=100000000` was a
|
|
12
|
+
* single-request table scan on every generated endpoint.
|
|
13
|
+
*
|
|
14
|
+
* This is the one parser they all share. It is deliberately strict on the input
|
|
15
|
+
* (a bound is a non-negative integer or it is a client error) and deliberately
|
|
16
|
+
* forgiving on magnitude (an oversized page is clamped, not rejected, so a
|
|
17
|
+
* caller paging with a too-large window still makes progress instead of
|
|
18
|
+
* receiving an error it cannot act on).
|
|
19
|
+
*
|
|
20
|
+
* The bounds themselves are also available to `SmrtCollection.list()` through
|
|
21
|
+
* the `defaultListLimit` / `maxListLimit` collection options. Those are opt-in:
|
|
22
|
+
* `list()` is the framework's own bulk-read primitive and dozens of internal
|
|
23
|
+
* callers (relationship loaders, junction hydration, `listByIds`) legitimately
|
|
24
|
+
* expect every matching row, so a framework-wide implicit default would
|
|
25
|
+
* silently truncate results rather than bound an attack surface. The bound
|
|
26
|
+
* belongs where untrusted input arrives, which is why the generated surfaces
|
|
27
|
+
* apply it unconditionally and direct programmatic callers opt in.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Page size a generated list surface uses when the caller supplies none.
|
|
31
|
+
*
|
|
32
|
+
* Matches the value the REST and SvelteKit generators already hard-coded, so
|
|
33
|
+
* adopting the shared parser is not a behavior change for callers that omit
|
|
34
|
+
* `limit`.
|
|
35
|
+
*/
|
|
36
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
37
|
+
/**
|
|
38
|
+
* Largest page a generated list surface will serve.
|
|
39
|
+
*
|
|
40
|
+
* Matches the cap `MCPGenerator` already applied in-process, now enforced on
|
|
41
|
+
* every generated surface rather than one of them.
|
|
42
|
+
*/
|
|
43
|
+
var MAX_LIST_LIMIT = 1e3;
|
|
44
|
+
/**
|
|
45
|
+
* A `limit`/`offset` value that cannot be honored.
|
|
46
|
+
*
|
|
47
|
+
* Extends `ValidationError` so the retry classifier never retries it, and
|
|
48
|
+
* carries `status`/`publicMessage` so `normalizeTypedHttpError()` renders it as
|
|
49
|
+
* a structured 400 instead of letting a caller typo become a 500.
|
|
50
|
+
*/
|
|
51
|
+
var QueryBoundsError = class extends ValidationError {
|
|
52
|
+
status = 400;
|
|
53
|
+
publicMessage;
|
|
54
|
+
constructor(message, code = "INVALID_QUERY_BOUNDS", details) {
|
|
55
|
+
super(message, code, details);
|
|
56
|
+
this.name = "QueryBoundsError";
|
|
57
|
+
this.publicMessage = message;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* An `orderBy` term that cannot be honored (#2367).
|
|
62
|
+
*
|
|
63
|
+
* `orderBy` reaches SQL in the identifier position, and until this rail existed
|
|
64
|
+
* it was checked only against `/^[a-zA-Z0-9_]+$/` — so an unknown column
|
|
65
|
+
* produced a driver error and a *sensitive* column produced a working ordering
|
|
66
|
+
* oracle: `?orderBy=api_secret&limit=1` reveals the extreme value of a secret
|
|
67
|
+
* one request at a time, even though the column itself is never serialized.
|
|
68
|
+
* `where` and `select` have rejected sensitive fields since #1540/#1902; this
|
|
69
|
+
* is the same rail for the third identifier-position input.
|
|
70
|
+
*/
|
|
71
|
+
var QueryOrderByError = class extends ValidationError {
|
|
72
|
+
status = 400;
|
|
73
|
+
publicMessage;
|
|
74
|
+
constructor(message, publicMessage, code = "INVALID_ORDER_BY", details) {
|
|
75
|
+
super(message, code, details);
|
|
76
|
+
this.name = "QueryOrderByError";
|
|
77
|
+
this.publicMessage = publicMessage;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Parse one bound from untrusted input.
|
|
82
|
+
*
|
|
83
|
+
* Accepts a non-negative safe integer, or a string of digits (optionally
|
|
84
|
+
* surrounded by whitespace). Everything else — `'abc'`, `'1.5'`, `'-1'`,
|
|
85
|
+
* `'1e3'`, `NaN`, `Infinity`, booleans, objects — is a client error, because
|
|
86
|
+
* silently substituting a default for a malformed bound hides the caller's bug
|
|
87
|
+
* and returns a page they did not ask for.
|
|
88
|
+
*
|
|
89
|
+
* `0` is preserved rather than folded into the default: `limit=0` means "no
|
|
90
|
+
* rows", which is what the REST surface already did and what the SvelteKit
|
|
91
|
+
* surface's `|| 50` fallback got wrong.
|
|
92
|
+
*/
|
|
93
|
+
function resolveListBound(raw, options) {
|
|
94
|
+
const { defaultValue = 0, maxValue, parameterName = "bound" } = options;
|
|
95
|
+
if (raw === null || raw === void 0 || raw === "") return maxValue !== void 0 ? Math.min(defaultValue, maxValue) : defaultValue;
|
|
96
|
+
let parsed;
|
|
97
|
+
if (typeof raw === "number") parsed = raw;
|
|
98
|
+
else if (typeof raw === "string") {
|
|
99
|
+
const trimmed = raw.trim();
|
|
100
|
+
if (!/^\d+$/.test(trimmed)) throw new QueryBoundsError(`Invalid ${parameterName}: '${raw}' is not a non-negative integer.`, "INVALID_QUERY_BOUNDS", {
|
|
101
|
+
parameterName,
|
|
102
|
+
value: raw
|
|
103
|
+
});
|
|
104
|
+
parsed = Number(trimmed);
|
|
105
|
+
} else throw new QueryBoundsError(`Invalid ${parameterName}: expected a non-negative integer, got ${typeof raw}.`, "INVALID_QUERY_BOUNDS", {
|
|
106
|
+
parameterName,
|
|
107
|
+
value: raw
|
|
108
|
+
});
|
|
109
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new QueryBoundsError(`Invalid ${parameterName}: '${String(raw)}' is not a non-negative integer.`, "INVALID_QUERY_BOUNDS", {
|
|
110
|
+
parameterName,
|
|
111
|
+
value: raw
|
|
112
|
+
});
|
|
113
|
+
return maxValue !== void 0 ? Math.min(parsed, maxValue) : parsed;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a page size from untrusted input: default when absent, rejected when
|
|
117
|
+
* malformed, clamped when oversized.
|
|
118
|
+
*
|
|
119
|
+
* @throws {QueryBoundsError} when the input is not a non-negative integer
|
|
120
|
+
*/
|
|
121
|
+
function resolveListLimit(raw, options = {}) {
|
|
122
|
+
return resolveListBound(raw, {
|
|
123
|
+
defaultValue: options.defaultValue ?? 50,
|
|
124
|
+
maxValue: options.maxValue ?? 1e3,
|
|
125
|
+
parameterName: options.parameterName ?? "limit"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a page offset from untrusted input.
|
|
130
|
+
*
|
|
131
|
+
* Unbounded above by default: an offset past the end of the table returns an
|
|
132
|
+
* empty page, and capping it would silently serve the wrong page instead.
|
|
133
|
+
*
|
|
134
|
+
* @throws {QueryBoundsError} when the input is not a non-negative integer
|
|
135
|
+
*/
|
|
136
|
+
function resolveListOffset(raw, options = {}) {
|
|
137
|
+
return resolveListBound(raw, {
|
|
138
|
+
defaultValue: options.defaultValue ?? 0,
|
|
139
|
+
maxValue: options.maxValue,
|
|
140
|
+
parameterName: options.parameterName ?? "offset"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Deterministic default ordering for a generated list page.
|
|
145
|
+
*
|
|
146
|
+
* `LIMIT`/`OFFSET` without `ORDER BY` is not pagination: PostgreSQL may return
|
|
147
|
+
* rows in any order and is free to choose a different one per query, so page 2
|
|
148
|
+
* can repeat or skip rows from page 1. `created_at DESC` alone is not enough
|
|
149
|
+
* either — rows sharing a timestamp still tie — so the primary key follows as a
|
|
150
|
+
* total-order tiebreak.
|
|
151
|
+
*
|
|
152
|
+
* Schema generation emits the index that makes this ordering cheap —
|
|
153
|
+
* `(tenant_id, created_at)` on a tenant-scoped table, `(created_at)` otherwise
|
|
154
|
+
* (#2363). The primary-key tiebreak is deliberately not in that index: its
|
|
155
|
+
* direction is opposite, so it is resolved by an incremental sort over the
|
|
156
|
+
* rows that share a timestamp.
|
|
157
|
+
*/
|
|
158
|
+
var DEFAULT_LIST_ORDER_BY = ["created_at DESC", "id ASC"];
|
|
159
|
+
/**
|
|
160
|
+
* Deterministic default ordering for a model whose primary key may not be `id`.
|
|
161
|
+
*
|
|
162
|
+
* A class declaring `@field({ primaryKey: true })` on its own column has no
|
|
163
|
+
* synthetic `id` column — schema generation omits `id`/`slug`/`context` for
|
|
164
|
+
* custom-primary-key classes — so `ORDER BY ... id ASC` would name a column
|
|
165
|
+
* that does not exist and fail the whole page. The tiebreak therefore follows
|
|
166
|
+
* the declared key.
|
|
167
|
+
*
|
|
168
|
+
* @param primaryKeyField - the model's primary-key field name in SMRT (not
|
|
169
|
+
* column) form; `collection.list()` maps it to the column
|
|
170
|
+
*/
|
|
171
|
+
function buildDefaultListOrderBy(primaryKeyField = "id") {
|
|
172
|
+
return ["created_at DESC", `${primaryKeyField} ASC`];
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { DEFAULT_LIST_LIMIT, DEFAULT_LIST_ORDER_BY, MAX_LIST_LIMIT, QueryBoundsError, QueryOrderByError, buildDefaultListOrderBy, resolveListLimit, resolveListOffset };
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=query-bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-bounds.js","names":[],"sources":["../src/query-bounds.ts"],"sourcesContent":["/**\n * Query bounds for list surfaces (#2367).\n *\n * Every generated read surface — REST, SvelteKit routes, the MCP server and the\n * MCP runtime bootstrap — takes `limit`/`offset` straight from untrusted input.\n * Before this module each one parsed them by hand, and each got it wrong in a\n * different way: `Number.parseInt('abc')` produced `LIMIT NaN` (a 500 from the\n * driver, not a 400 from the API), `Number(x) || 50` silently turned `0` into\n * `50`, and none of them capped the value, so `?limit=100000000` was a\n * single-request table scan on every generated endpoint.\n *\n * This is the one parser they all share. It is deliberately strict on the input\n * (a bound is a non-negative integer or it is a client error) and deliberately\n * forgiving on magnitude (an oversized page is clamped, not rejected, so a\n * caller paging with a too-large window still makes progress instead of\n * receiving an error it cannot act on).\n *\n * The bounds themselves are also available to `SmrtCollection.list()` through\n * the `defaultListLimit` / `maxListLimit` collection options. Those are opt-in:\n * `list()` is the framework's own bulk-read primitive and dozens of internal\n * callers (relationship loaders, junction hydration, `listByIds`) legitimately\n * expect every matching row, so a framework-wide implicit default would\n * silently truncate results rather than bound an attack surface. The bound\n * belongs where untrusted input arrives, which is why the generated surfaces\n * apply it unconditionally and direct programmatic callers opt in.\n */\n\nimport { ValidationError } from './errors';\n\n/**\n * Page size a generated list surface uses when the caller supplies none.\n *\n * Matches the value the REST and SvelteKit generators already hard-coded, so\n * adopting the shared parser is not a behavior change for callers that omit\n * `limit`.\n */\nexport const DEFAULT_LIST_LIMIT = 50;\n\n/**\n * Largest page a generated list surface will serve.\n *\n * Matches the cap `MCPGenerator` already applied in-process, now enforced on\n * every generated surface rather than one of them.\n */\nexport const MAX_LIST_LIMIT = 1000;\n\n/**\n * A `limit`/`offset` value that cannot be honored.\n *\n * Extends `ValidationError` so the retry classifier never retries it, and\n * carries `status`/`publicMessage` so `normalizeTypedHttpError()` renders it as\n * a structured 400 instead of letting a caller typo become a 500.\n */\nexport class QueryBoundsError extends ValidationError {\n readonly status = 400;\n readonly publicMessage: string;\n\n constructor(\n message: string,\n code = 'INVALID_QUERY_BOUNDS',\n details?: Record<string, unknown>,\n ) {\n super(message, code, details);\n this.name = 'QueryBoundsError';\n this.publicMessage = message;\n }\n}\n\n/**\n * An `orderBy` term that cannot be honored (#2367).\n *\n * `orderBy` reaches SQL in the identifier position, and until this rail existed\n * it was checked only against `/^[a-zA-Z0-9_]+$/` — so an unknown column\n * produced a driver error and a *sensitive* column produced a working ordering\n * oracle: `?orderBy=api_secret&limit=1` reveals the extreme value of a secret\n * one request at a time, even though the column itself is never serialized.\n * `where` and `select` have rejected sensitive fields since #1540/#1902; this\n * is the same rail for the third identifier-position input.\n */\nexport class QueryOrderByError extends ValidationError {\n readonly status = 400;\n readonly publicMessage: string;\n\n constructor(\n message: string,\n publicMessage: string,\n code = 'INVALID_ORDER_BY',\n details?: Record<string, unknown>,\n ) {\n super(message, code, details);\n this.name = 'QueryOrderByError';\n this.publicMessage = publicMessage;\n }\n}\n\n/**\n * Options for {@link resolveListLimit} / {@link resolveListOffset}.\n */\nexport interface ListBoundOptions {\n /** Value used when the input is absent (`null`, `undefined`, or `''`). */\n defaultValue?: number;\n /** Upper bound; larger values are clamped down to it. Omit for none. */\n maxValue?: number;\n /** Parameter name used in the rejection message. */\n parameterName?: string;\n}\n\n/**\n * Parse one bound from untrusted input.\n *\n * Accepts a non-negative safe integer, or a string of digits (optionally\n * surrounded by whitespace). Everything else — `'abc'`, `'1.5'`, `'-1'`,\n * `'1e3'`, `NaN`, `Infinity`, booleans, objects — is a client error, because\n * silently substituting a default for a malformed bound hides the caller's bug\n * and returns a page they did not ask for.\n *\n * `0` is preserved rather than folded into the default: `limit=0` means \"no\n * rows\", which is what the REST surface already did and what the SvelteKit\n * surface's `|| 50` fallback got wrong.\n */\nfunction resolveListBound(raw: unknown, options: ListBoundOptions): number {\n const { defaultValue = 0, maxValue, parameterName = 'bound' } = options;\n\n if (raw === null || raw === undefined || raw === '') {\n return maxValue !== undefined\n ? Math.min(defaultValue, maxValue)\n : defaultValue;\n }\n\n let parsed: number;\n if (typeof raw === 'number') {\n parsed = raw;\n } else if (typeof raw === 'string') {\n const trimmed = raw.trim();\n // Strict digits only. `Number.parseInt` would accept '12abc' and\n // `Number()` would accept '1e3' and ' 0x10 '; both would then be\n // interpolated into a page size the caller never wrote.\n if (!/^\\d+$/.test(trimmed)) {\n throw new QueryBoundsError(\n `Invalid ${parameterName}: '${raw}' is not a non-negative integer.`,\n 'INVALID_QUERY_BOUNDS',\n { parameterName, value: raw },\n );\n }\n parsed = Number(trimmed);\n } else {\n throw new QueryBoundsError(\n `Invalid ${parameterName}: expected a non-negative integer, got ${typeof raw}.`,\n 'INVALID_QUERY_BOUNDS',\n { parameterName, value: raw },\n );\n }\n\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new QueryBoundsError(\n `Invalid ${parameterName}: '${String(raw)}' is not a non-negative integer.`,\n 'INVALID_QUERY_BOUNDS',\n { parameterName, value: raw },\n );\n }\n\n return maxValue !== undefined ? Math.min(parsed, maxValue) : parsed;\n}\n\n/**\n * Resolve a page size from untrusted input: default when absent, rejected when\n * malformed, clamped when oversized.\n *\n * @throws {QueryBoundsError} when the input is not a non-negative integer\n */\nexport function resolveListLimit(\n raw: unknown,\n options: ListBoundOptions = {},\n): number {\n return resolveListBound(raw, {\n defaultValue: options.defaultValue ?? DEFAULT_LIST_LIMIT,\n maxValue: options.maxValue ?? MAX_LIST_LIMIT,\n parameterName: options.parameterName ?? 'limit',\n });\n}\n\n/**\n * Resolve a page offset from untrusted input.\n *\n * Unbounded above by default: an offset past the end of the table returns an\n * empty page, and capping it would silently serve the wrong page instead.\n *\n * @throws {QueryBoundsError} when the input is not a non-negative integer\n */\nexport function resolveListOffset(\n raw: unknown,\n options: ListBoundOptions = {},\n): number {\n return resolveListBound(raw, {\n defaultValue: options.defaultValue ?? 0,\n maxValue: options.maxValue,\n parameterName: options.parameterName ?? 'offset',\n });\n}\n\n/**\n * Deterministic default ordering for a generated list page.\n *\n * `LIMIT`/`OFFSET` without `ORDER BY` is not pagination: PostgreSQL may return\n * rows in any order and is free to choose a different one per query, so page 2\n * can repeat or skip rows from page 1. `created_at DESC` alone is not enough\n * either — rows sharing a timestamp still tie — so the primary key follows as a\n * total-order tiebreak.\n *\n * Schema generation emits the index that makes this ordering cheap —\n * `(tenant_id, created_at)` on a tenant-scoped table, `(created_at)` otherwise\n * (#2363). The primary-key tiebreak is deliberately not in that index: its\n * direction is opposite, so it is resolved by an incremental sort over the\n * rows that share a timestamp.\n */\nexport const DEFAULT_LIST_ORDER_BY: readonly string[] = [\n 'created_at DESC',\n 'id ASC',\n];\n\n/**\n * Deterministic default ordering for a model whose primary key may not be `id`.\n *\n * A class declaring `@field({ primaryKey: true })` on its own column has no\n * synthetic `id` column — schema generation omits `id`/`slug`/`context` for\n * custom-primary-key classes — so `ORDER BY ... id ASC` would name a column\n * that does not exist and fail the whole page. The tiebreak therefore follows\n * the declared key.\n *\n * @param primaryKeyField - the model's primary-key field name in SMRT (not\n * column) form; `collection.list()` maps it to the column\n */\nexport function buildDefaultListOrderBy(primaryKeyField = 'id'): string[] {\n return ['created_at DESC', `${primaryKeyField} ASC`];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,qBAAqB;;;;;;;AAQlC,IAAa,iBAAiB;;;;;;;;AAS9B,IAAa,mBAAb,cAAsC,gBAAgB;CACpD,SAAkB;CAClB;CAEA,YACE,SACA,OAAO,wBACP,SACA;EACA,MAAM,SAAS,MAAM,OAAO;EAC5B,KAAK,OAAO;EACZ,KAAK,gBAAgB;CACvB;AACF;;;;;;;;;;;;AAaA,IAAa,oBAAb,cAAuC,gBAAgB;CACrD,SAAkB;CAClB;CAEA,YACE,SACA,eACA,OAAO,oBACP,SACA;EACA,MAAM,SAAS,MAAM,OAAO;EAC5B,KAAK,OAAO;EACZ,KAAK,gBAAgB;CACvB;AACF;;;;;;;;;;;;;;AA2BA,SAAS,iBAAiB,KAAc,SAAmC;CACzE,MAAM,EAAE,eAAe,GAAG,UAAU,gBAAgB,YAAY;CAEhE,IAAI,QAAQ,QAAQ,QAAQ,KAAA,KAAa,QAAQ,IAC/C,OAAO,aAAa,KAAA,IAChB,KAAK,IAAI,cAAc,QAAQ,IAC/B;CAGN,IAAI;CACJ,IAAI,OAAO,QAAQ,UACjB,SAAS;MACJ,IAAI,OAAO,QAAQ,UAAU;EAClC,MAAM,UAAU,IAAI,KAAK;EAIzB,IAAI,CAAC,QAAQ,KAAK,OAAO,GACvB,MAAM,IAAI,iBACR,WAAW,cAAc,KAAK,IAAI,mCAClC,wBACA;GAAE;GAAe,OAAO;EAAI,CAC9B;EAEF,SAAS,OAAO,OAAO;CACzB,OACE,MAAM,IAAI,iBACR,WAAW,cAAc,yCAAyC,OAAO,IAAI,IAC7E,wBACA;EAAE;EAAe,OAAO;CAAI,CAC9B;CAGF,IAAI,CAAC,OAAO,cAAc,MAAM,KAAK,SAAS,GAC5C,MAAM,IAAI,iBACR,WAAW,cAAc,KAAK,OAAO,GAAG,EAAE,mCAC1C,wBACA;EAAE;EAAe,OAAO;CAAI,CAC9B;CAGF,OAAO,aAAa,KAAA,IAAY,KAAK,IAAI,QAAQ,QAAQ,IAAI;AAC/D;;;;;;;AAQA,SAAgB,iBACd,KACA,UAA4B,CAAC,GACrB;CACR,OAAO,iBAAiB,KAAK;EAC3B,cAAc,QAAQ,gBAAA;EACtB,UAAU,QAAQ,YAAA;EAClB,eAAe,QAAQ,iBAAiB;CAC1C,CAAC;AACH;;;;;;;;;AAUA,SAAgB,kBACd,KACA,UAA4B,CAAC,GACrB;CACR,OAAO,iBAAiB,KAAK;EAC3B,cAAc,QAAQ,gBAAgB;EACtC,UAAU,QAAQ;EAClB,eAAe,QAAQ,iBAAiB;CAC1C,CAAC;AACH;;;;;;;;;;;;;;;;AAiBA,IAAa,wBAA2C,CACtD,mBACA,QACF;;;;;;;;;;;;;AAcA,SAAgB,wBAAwB,kBAAkB,MAAgB;CACxE,OAAO,CAAC,mBAAmB,GAAG,gBAAgB,KAAK;AACrD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"class-registration.d.ts","sourceRoot":"","sources":["../../src/registry/class-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAUH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAKV,qBAAqB,EAGtB,MAAM,qBAAqB,CAAC;AAqC7B,OAAO,KAAK,EACV,eAAe,EAEf,iBAAiB,EAElB,MAAM,YAAY,CAAC;AA0dpB,wBAAgB,QAAQ,CACtB,IAAI,EAAE,OAAO,UAAU,EACvB,MAAM,GAAE,iBAAsB,GAC7B,IAAI,CAmvBN;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAMlB,qBAAqB,EAAE,WAAW,CAAC,eAAe,CAAC,uBAAuB,CAAC,CAAC,GAC3E,IAAI,CAeN;AA6LD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,eAAe,EACzB,gBAAgB,CAAC,EAAE;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CA2CN;AA0ID,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,qBAAqB,EAChC,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,
|
|
1
|
+
{"version":3,"file":"class-registration.d.ts","sourceRoot":"","sources":["../../src/registry/class-registration.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAUH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAKV,qBAAqB,EAGtB,MAAM,qBAAqB,CAAC;AAqC7B,OAAO,KAAK,EACV,eAAe,EAEf,iBAAiB,EAElB,MAAM,YAAY,CAAC;AA0dpB,wBAAgB,QAAQ,CACtB,IAAI,EAAE,OAAO,UAAU,EACvB,MAAM,GAAE,iBAAsB,GAC7B,IAAI,CAmvBN;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAMlB,qBAAqB,EAAE,WAAW,CAAC,eAAe,CAAC,uBAAuB,CAAC,CAAC,GAC3E,IAAI,CAeN;AA6LD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,eAAe,EACzB,gBAAgB,CAAC,EAAE;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CA2CN;AA0ID,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,qBAAqB,EAChC,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CA4QN"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ConfigurationError } from "../errors.js";
|
|
2
|
+
import { toSnakeCase } from "../utils/naming.js";
|
|
2
3
|
import { createQualifiedName, isQualifiedName } from "../utils/qualified-names.js";
|
|
3
4
|
import { cloneManifestSchemaColumns } from "../manifest/store.js";
|
|
4
5
|
import { getClasses, getCollections, getConstructorIndex, getFieldDecorators, getInheritanceCache, getSourceFileFromStack, getStiSiblingsLoaded, verboseLog } from "./shared-state.js";
|
|
5
6
|
import { findClass, getCanonicalClassName, hasClassCaseInsensitive, qualifyExtendsName } from "./name-resolver.js";
|
|
6
7
|
import { buildInheritanceChain } from "./inheritance-resolver.js";
|
|
7
|
-
import { toSnakeCase } from "../utils/naming.js";
|
|
8
8
|
import { discoverManifestSync, discoverSTISiblingsSync, getPackageName, lookupInManifest } from "../manifest/manifest-loader.js";
|
|
9
9
|
import { tableNameFromClass } from "../utils.js";
|
|
10
10
|
import { SmrtObject } from "../object.js";
|
|
@@ -882,6 +882,7 @@ function registerFromManifest(name, objectDef, packageName) {
|
|
|
882
882
|
} else verboseLog(`[registry] Using ${validationRules.length} pre-computed validation rules for ${name}`);
|
|
883
883
|
const qualifiedName = qualifiedNameEarly;
|
|
884
884
|
const visibility = objectDef.visibility || config.visibility || "public";
|
|
885
|
+
const tenantScopedConfig = normalizeTenantScopedConfig(config.tenantScoped);
|
|
885
886
|
getClasses().set(registrationKey, {
|
|
886
887
|
name: simpleClassName,
|
|
887
888
|
qualifiedName,
|
|
@@ -890,6 +891,7 @@ function registerFromManifest(name, objectDef, packageName) {
|
|
|
890
891
|
fields,
|
|
891
892
|
methods,
|
|
892
893
|
schema,
|
|
894
|
+
tenantScopedConfig,
|
|
893
895
|
validators,
|
|
894
896
|
validationRules,
|
|
895
897
|
tools: objectDef.tools,
|