@stacksjs/orm 0.70.293 → 0.70.296
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/dist/auto-crud.d.ts +62 -4
- package/dist/define-model.d.ts +35 -1
- package/dist/index.js +13 -13
- package/package.json +4 -4
package/dist/auto-crud.d.ts
CHANGED
|
@@ -137,14 +137,53 @@ declare function safeJSONOrEmpty(_s: string): unknown;
|
|
|
137
137
|
* read path) now gets its casts instead of leaking raw SQLite `"1"`s.
|
|
138
138
|
*/
|
|
139
139
|
export declare function applyCasts(record: Record<string, any> | null | undefined, casts: Record<string, string | { get: (v: unknown) => unknown, set: (v: unknown) => unknown }> | null | undefined, direction: 'get' | 'set'): any;
|
|
140
|
+
export declare function validateWriteBody(data: Record<string, any>, model: any, hook: 'creating' | 'updating'): WriteValidationResult;
|
|
141
|
+
/**
|
|
142
|
+
* A route path with every parameter name flattened to `{}`.
|
|
143
|
+
*
|
|
144
|
+
* The "user routes win" guard compared paths literally, so an app's own
|
|
145
|
+
* `/api/sites/{siteId}` did not suppress the ORM's `/api/sites/{id}` — the two
|
|
146
|
+
* strings differ, so BOTH were registered and the ORM copy carried none of the
|
|
147
|
+
* app's authorization. The app had declared the endpoint and still got a second,
|
|
148
|
+
* unguarded one it never wrote (stacksjs/stacks#2224).
|
|
149
|
+
*
|
|
150
|
+
* The parameter's NAME is the app's business. The shape is what decides whether
|
|
151
|
+
* this URL is already claimed.
|
|
152
|
+
*/
|
|
153
|
+
export declare function routeShape(path: string): string;
|
|
140
154
|
/**
|
|
141
155
|
* Resolve middleware lists for a model's `useApi` trait value (which may be
|
|
142
156
|
* `true` or `{ uri, routes, middleware }`).
|
|
143
157
|
*
|
|
144
|
-
* Secure-by-default:
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
158
|
+
* Secure-by-default on BOTH sides: with no declared `useApi.middleware`, read
|
|
159
|
+
* and mutating routes alike get `auth`.
|
|
160
|
+
*
|
|
161
|
+
* #1949 gave the mutating routes that default and deliberately left reads
|
|
162
|
+
* public, reasoning that catalog tables (products, posts) want anonymous
|
|
163
|
+
* browsing. The cost of that default landed on models that are not catalogs: a
|
|
164
|
+
* model opting into the trait without declaring middleware published
|
|
165
|
+
* `GET /api/{uri}` and `GET /api/{uri}/{id}` to anyone. In one real app that
|
|
166
|
+
* was `GET /api/users` returning the full customer list — only `password` was
|
|
167
|
+
* `hidden`, so names and emails came back — and the app's own security tests
|
|
168
|
+
* could not see it, because the route was never declared in its route files
|
|
169
|
+
* (stacksjs/stacks#2224).
|
|
170
|
+
*
|
|
171
|
+
* A wrong "public" default is a data breach; a wrong "private" default is a 401
|
|
172
|
+
* on the first request in development. Only one of those is recoverable, so the
|
|
173
|
+
* default is now `auth` and a public read is something an app asks for.
|
|
174
|
+
*
|
|
175
|
+
* Three declaration shapes, so asking is always possible:
|
|
176
|
+
*
|
|
177
|
+
* `middleware: ['auth']` both sides get the list (unchanged)
|
|
178
|
+
* `middleware: []` both sides public — deliberate opt-out,
|
|
179
|
+
* warned about at the call site
|
|
180
|
+
* `middleware: { read, write }` per-side lists
|
|
181
|
+
*
|
|
182
|
+
* The split form exists because the secure default would otherwise make the
|
|
183
|
+
* most common real shape — public catalog reads, authenticated writes —
|
|
184
|
+
* inexpressible: a flat `middleware: []` is the only way to open reads, and it
|
|
185
|
+
* opens writes at the same time. That is a worse trade than the bug being fixed,
|
|
186
|
+
* so `{ read: [], write: ['auth'] }` says it exactly.
|
|
148
187
|
*/
|
|
149
188
|
export declare function resolveApiMiddleware(useApi: unknown): { read: string[], write: string[], declared: boolean };
|
|
150
189
|
/**
|
|
@@ -268,3 +307,22 @@ export declare interface IndexPaginator {
|
|
|
268
307
|
first_page_url?: string
|
|
269
308
|
last_page_url?: string
|
|
270
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Run each declared `validation.rule` against a write payload.
|
|
312
|
+
*
|
|
313
|
+
* Returns `{ valid: true }` or `{ valid: false, errors }`. Per-attribute custom
|
|
314
|
+
* messages from `validation.message` override the rule's default text.
|
|
315
|
+
*
|
|
316
|
+
* Fields the caller never sent are skipped on the `updating` hook, so a partial
|
|
317
|
+
* update does not trip a `required` rule on a sibling field it never touched.
|
|
318
|
+
*
|
|
319
|
+
* Lives here rather than in `../routes.ts` so BOTH write paths can reach it.
|
|
320
|
+
* It used to be a local function in that module, which meant the declared rules
|
|
321
|
+
* ran on the generated REST routes and nowhere else: `Model.create()`,
|
|
322
|
+
* `.update()` and `.save()` went straight to the driver, and an over-length
|
|
323
|
+
* value first got noticed by Postgres as a 22001, surfacing as a 500 on
|
|
324
|
+
* whichever endpoint performed the write (stacksjs/stacks#2233). Importing it
|
|
325
|
+
* from `routes.ts` was not an option — that module registers routes on import.
|
|
326
|
+
*/
|
|
327
|
+
export type WriteValidationResult = | { valid: true }
|
|
328
|
+
| { valid: false, errors: Record<string, string[]> }
|
package/dist/define-model.d.ts
CHANGED
|
@@ -22,6 +22,17 @@ export type { ModelDefinition, InferRelationNames, ModelAttributes, InferModelAt
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export declare function withoutEvents<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
25
|
+
/**
|
|
26
|
+
* Run a callback with model validation suppressed for its entire duration.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* await User.withoutValidation(async () => {
|
|
31
|
+
* for (const row of legacyRows) await User.create(row) // rules do not run
|
|
32
|
+
* })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function withoutValidation<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
25
36
|
export declare function defineModel<const TDef extends ModelDefinition>(definition: TDef): StacksModelStatic<TDef>;
|
|
26
37
|
/**
|
|
27
38
|
* Normalize a ModelInstance (or array of them, or already-plain row) into
|
|
@@ -49,7 +60,13 @@ declare interface StacksModelDefinition extends Omit<BQBModelDefinition, 'attrib
|
|
|
49
60
|
table: string
|
|
50
61
|
primaryKey?: string
|
|
51
62
|
autoIncrement?: boolean
|
|
52
|
-
traits?: NonNullable<BQBModelDefinition['traits']> &
|
|
63
|
+
traits?: Omit<NonNullable<BQBModelDefinition['traits']>, 'useApi'> & {
|
|
64
|
+
useApi?: boolean | {
|
|
65
|
+
readonly uri?: string
|
|
66
|
+
readonly routes?: readonly string[]
|
|
67
|
+
readonly middleware?: any
|
|
68
|
+
}
|
|
69
|
+
} & Record<string, unknown>
|
|
53
70
|
indexes?: Array<{ name: string, columns: string[], unique?: boolean, where?: string }>
|
|
54
71
|
casts?: Record<string, CastType | CasterInterface>
|
|
55
72
|
attributes: {
|
|
@@ -146,6 +163,23 @@ export type StacksModelStatic<TDef extends ModelDefinition> = OrmModelStatic<TDe
|
|
|
146
163
|
forceCreate: (data: Record<string, unknown>) => ReturnType<OrmModelStatic<TDef>['create']>
|
|
147
164
|
delete: (id: number | string) => Promise<boolean>
|
|
148
165
|
withoutEvents: <T>(fn: () => T | Promise<T>) => Promise<T>
|
|
166
|
+
/** Run `fn` with declared `validation.rule`s suppressed (bulk imports, backfills). */
|
|
167
|
+
withoutValidation: <T>(fn: () => T | Promise<T>) => Promise<T>
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Thrown when a direct write fails a declared `validation.rule`.
|
|
171
|
+
*
|
|
172
|
+
* Carries `status = 422` and a per-field `errors` map, matching the shape the
|
|
173
|
+
* generated REST routes already return, so a handler that catches this can
|
|
174
|
+
* respond with the same body it would have produced through auto-CRUD. It is
|
|
175
|
+
* duck-typed by `mapWriteError`, which preserves any integer `status` in
|
|
176
|
+
* 400-599 — so an over-length value now surfaces as a 422 instead of the
|
|
177
|
+
* driver's raw 22001 becoming a 500 (stacksjs/stacks#2233).
|
|
178
|
+
*/
|
|
179
|
+
export declare class ModelValidationError extends Error {
|
|
180
|
+
readonly status: number;
|
|
181
|
+
readonly errors: Record<string, string[]>;
|
|
182
|
+
constructor(modelName: string, errors: Record<string, string[]>);
|
|
149
183
|
}
|
|
150
184
|
/**
|
|
151
185
|
* Thrown by `Model.findOrFail(id)` (and other strict lookups) when no row matches.
|