@marianmeres/ownsuite 1.0.3 → 2.1.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 +206 -21
- package/API.md +410 -18
- package/README.md +86 -2
- package/dist/adapters/mock-auth.d.ts +38 -0
- package/dist/adapters/mock-auth.js +237 -0
- package/dist/adapters/mock.d.ts +10 -3
- package/dist/adapters/mock.js +79 -25
- package/dist/adapters/mod.d.ts +2 -0
- package/dist/adapters/mod.js +2 -0
- package/dist/adapters/stack-account.d.ts +38 -0
- package/dist/adapters/stack-account.js +149 -0
- package/dist/domains/auth.d.ts +83 -0
- package/dist/domains/auth.js +211 -0
- package/dist/domains/base.d.ts +66 -10
- package/dist/domains/base.js +165 -13
- package/dist/domains/mod.d.ts +3 -0
- package/dist/domains/mod.js +3 -0
- package/dist/domains/owned-collection.d.ts +29 -4
- package/dist/domains/owned-collection.js +240 -120
- package/dist/domains/profile.d.ts +62 -0
- package/dist/domains/profile.js +170 -0
- package/dist/domains/session.d.ts +73 -0
- package/dist/domains/session.js +226 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/oauth/popup.d.ts +64 -0
- package/dist/oauth/popup.js +104 -0
- package/dist/ownsuite.d.ts +58 -5
- package/dist/ownsuite.js +178 -10
- package/dist/types/adapter.d.ts +4 -0
- package/dist/types/auth.d.ts +162 -0
- package/dist/types/auth.js +17 -0
- package/dist/types/events.d.ts +41 -2
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.js +1 -0
- package/dist/types/state.d.ts +17 -0
- package/docs/future-improvements.md +81 -0
- package/package.json +15 -6
package/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ Machine-readable documentation for AI coding assistants.
|
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
8
8
|
name: "@marianmeres/ownsuite"
|
|
9
|
-
version: "
|
|
9
|
+
version: "2.0.0"
|
|
10
10
|
type: "library"
|
|
11
11
|
language: "typescript"
|
|
12
12
|
runtime: "deno"
|
|
@@ -19,25 +19,51 @@ entry: "./src/mod.ts"
|
|
|
19
19
|
|
|
20
20
|
Client-side helper library for **owner-scoped** UIs. Generic domain managers for CRUD over collections where every row is implicitly filtered to the authenticated subject by the server. Mirrors the shape of `@marianmeres/ecsuite` but applies to arbitrary owner-scoped collections instead of hard-coded e-commerce domains.
|
|
21
21
|
|
|
22
|
+
**Also (opt-in)** provides the blessed account-lifecycle surface for apps built on `@marianmeres/stack-account`: `suite.auth` (register / login / logout / OAuth init / password reset / delete account), `suite.profile` (`/me` CRUD + OAuth link list), `suite.session` (reactive JWT + subject, pluggable storage). Pass an `AuthAdapter` to `createOwnsuite({ adapters: { auth } })` to attach them.
|
|
23
|
+
|
|
22
24
|
Pairs with:
|
|
23
25
|
- **`@marianmeres/collection`** — `ownerIdScope` route hook (read-side owner enforcement).
|
|
24
26
|
- **`@marianmeres/stack-common`** — `ownsuiteOptions()` server helper for mounting `/me/*` routes.
|
|
27
|
+
- **`@marianmeres/stack-account`** — default adapters (`createStackAccountAuthAdapter`, `createStackAccountProfileAdapter`) target its REST surface.
|
|
25
28
|
|
|
26
29
|
## Architecture
|
|
27
30
|
|
|
28
31
|
```
|
|
29
32
|
Ownsuite (orchestrator)
|
|
30
|
-
├── #pubsub (shared event bus)
|
|
33
|
+
├── #pubsub (shared event bus, cleared on destroy)
|
|
31
34
|
├── #context (propagated to all domains on setContext)
|
|
32
35
|
└── domains: Map<string, OwnedCollectionManager>
|
|
33
|
-
├── store
|
|
34
|
-
├── adapter
|
|
35
|
-
├── state machine:
|
|
36
|
-
|
|
36
|
+
├── store (Svelte-compatible DomainStateWrapper<OwnedCollectionState<TRow>>)
|
|
37
|
+
├── adapter (OwnedCollectionAdapter)
|
|
38
|
+
├── state machine: initializing → ready ↔ syncing → error
|
|
39
|
+
├── optimistic update + per-row rollback on update/delete
|
|
40
|
+
├── mutation chain (serial create/update/delete)
|
|
41
|
+
├── abort-supersede (initialize/refresh — newer call aborts older)
|
|
42
|
+
└── destroy() (aborts in-flight ops, drops adapter)
|
|
37
43
|
```
|
|
38
44
|
|
|
39
45
|
Each domain holds one list of rows. List operations replace the list wholesale; single-row ops mutate it in place so subscribers see stable references.
|
|
40
46
|
|
|
47
|
+
### Concurrency model
|
|
48
|
+
|
|
49
|
+
- **Mutations serialize** per manager via an internal promise chain. A
|
|
50
|
+
`create/update/delete` that starts while another is in-flight queues
|
|
51
|
+
behind it; callers still receive their own result through the returned
|
|
52
|
+
promise. Rejections on the chain are swallowed so they do not block
|
|
53
|
+
later mutations.
|
|
54
|
+
- **Reads abort-supersede**: a new `initialize()` or `refresh()` aborts
|
|
55
|
+
any in-flight read on the same manager. The aborted call resolves
|
|
56
|
+
without writing to the store.
|
|
57
|
+
- **onSuccess uses live data, not a captured snapshot**, so interleaving
|
|
58
|
+
reads and mutations never resurrect deleted rows or clobber writes.
|
|
59
|
+
- **Rollback is per-row**: a failed `update` reverts just the updated
|
|
60
|
+
row; a failed `delete` re-inserts the deleted row at its original
|
|
61
|
+
position. An interleaved refresh that brought new rows is preserved.
|
|
62
|
+
- **AbortSignal plumbing**: every adapter call receives
|
|
63
|
+
`ctx.signal: AbortSignal`. `reset()` and `destroy()` abort all active
|
|
64
|
+
signals. Adapters should forward the signal to `fetch()` — ignoring
|
|
65
|
+
it is safe but leaves abandoned requests running.
|
|
66
|
+
|
|
41
67
|
## Directory Structure
|
|
42
68
|
|
|
43
69
|
```
|
|
@@ -47,17 +73,28 @@ src/
|
|
|
47
73
|
├── types/
|
|
48
74
|
│ ├── mod.ts
|
|
49
75
|
│ ├── state.ts # DomainState/Wrapper/Error, OwnsuiteContext, OwnedCollectionState
|
|
50
|
-
│ ├── events.ts # OwnsuiteEventType, OwnsuiteEvent union
|
|
51
|
-
│
|
|
76
|
+
│ ├── events.ts # OwnsuiteEventType, OwnsuiteEvent union (incl. auth:* / profile:* / oauth:*)
|
|
77
|
+
│ ├── adapter.ts # OwnedCollectionAdapter, OwnedListResult, OwnedRowResult
|
|
78
|
+
│ └── auth.ts # AuthAdapter, ProfileAdapter, SessionState/Subject/Status, OAuth*
|
|
52
79
|
├── domains/
|
|
53
80
|
│ ├── mod.ts
|
|
54
81
|
│ ├── base.ts # BaseDomainManager abstract class (mirrors ecsuite)
|
|
55
|
-
│
|
|
82
|
+
│ ├── owned-collection.ts # OwnedCollectionManager<TRow, TCreate, TUpdate>
|
|
83
|
+
│ ├── session.ts # SessionManager + pluggable SessionStorage resolver
|
|
84
|
+
│ ├── auth.ts # AuthManager (register/login/logout/OAuth/verify/delete)
|
|
85
|
+
│ └── profile.ts # ProfileManager (/me singleton)
|
|
86
|
+
├── oauth/
|
|
87
|
+
│ └── popup.ts # openOAuthPopup + injectable PopupWindowHost for tests
|
|
56
88
|
└── adapters/
|
|
57
89
|
├── mod.ts
|
|
58
|
-
|
|
90
|
+
├── mock.ts # createMockOwnedCollectionAdapter
|
|
91
|
+
├── mock-auth.ts # createMockAuthAdapter / createMockProfileAdapter / createMockAuthStore
|
|
92
|
+
└── stack-account.ts # createStackAccountAuthAdapter / createStackAccountProfileAdapter
|
|
59
93
|
tests/
|
|
60
|
-
|
|
94
|
+
├── ownsuite.test.ts # core suite + OwnedCollectionManager
|
|
95
|
+
├── concurrency.test.ts # critical-invariant coverage (abort-supersede, rollback, etc.)
|
|
96
|
+
├── auth.test.ts # AuthManager / ProfileManager / SessionManager
|
|
97
|
+
└── oauth-popup.test.ts # openOAuthPopup message / timeout / close / origin semantics
|
|
61
98
|
```
|
|
62
99
|
|
|
63
100
|
## Key Exports
|
|
@@ -65,7 +102,9 @@ tests/
|
|
|
65
102
|
```typescript
|
|
66
103
|
// Main
|
|
67
104
|
export { Ownsuite, createOwnsuite } from "./ownsuite.ts";
|
|
68
|
-
export type {
|
|
105
|
+
export type {
|
|
106
|
+
OwnsuiteConfig, OwnsuiteDomainConfig, SetContextOptions,
|
|
107
|
+
} from "./ownsuite.ts";
|
|
69
108
|
|
|
70
109
|
// Domain managers
|
|
71
110
|
export { BaseDomainManager, OwnedCollectionManager } from "./domains/mod.ts";
|
|
@@ -85,8 +124,65 @@ export type {
|
|
|
85
124
|
// Mock adapter (for tests)
|
|
86
125
|
export { createMockOwnedCollectionAdapter } from "./adapters/mod.ts";
|
|
87
126
|
export type { MockAdapterOptions } from "./adapters/mod.ts";
|
|
127
|
+
|
|
128
|
+
// Account lifecycle (optional — attached when adapters.auth is supplied)
|
|
129
|
+
export { SessionManager, AuthManager, ProfileManager } from "./domains/mod.ts";
|
|
130
|
+
export type {
|
|
131
|
+
AuthAdapter, ProfileAdapter, AuthTokenResult, ProfileResult,
|
|
132
|
+
SessionState, SessionSubject, SessionStatus,
|
|
133
|
+
SessionStorage, SessionStorageType,
|
|
134
|
+
OAuthConnection, OAuthProvider, OAuthInitOptions, OAuthAction,
|
|
135
|
+
} from "./types/mod.ts";
|
|
136
|
+
|
|
137
|
+
// OAuth popup helper
|
|
138
|
+
export { openOAuthPopup } from "./oauth/popup.ts";
|
|
139
|
+
export type {
|
|
140
|
+
OAuthPopupMessage, OAuthPopupLoginMessage, OAuthPopupLinkMessage,
|
|
141
|
+
OpenOAuthPopupOptions, PopupWindowHost, PopupWindowHandle,
|
|
142
|
+
} from "./oauth/popup.ts";
|
|
143
|
+
|
|
144
|
+
// Default stack-account adapters
|
|
145
|
+
export {
|
|
146
|
+
createStackAccountAuthAdapter,
|
|
147
|
+
createStackAccountProfileAdapter,
|
|
148
|
+
} from "./adapters/mod.ts";
|
|
149
|
+
export type { StackAccountAdapterOptions } from "./adapters/mod.ts";
|
|
150
|
+
|
|
151
|
+
// Mock auth adapter (for tests)
|
|
152
|
+
export {
|
|
153
|
+
createMockAuthAdapter, createMockProfileAdapter,
|
|
154
|
+
createMockAuthStore, verifyMockAccount,
|
|
155
|
+
} from "./adapters/mod.ts";
|
|
156
|
+
export type { MockAuthStore } from "./adapters/mod.ts";
|
|
88
157
|
```
|
|
89
158
|
|
|
159
|
+
## Account lifecycle (optional)
|
|
160
|
+
|
|
161
|
+
When `adapters.auth` is supplied, `createOwnsuite` instantiates three extra managers and attaches them as readonly suite properties:
|
|
162
|
+
|
|
163
|
+
- **`suite.session: SessionManager`** — reactive `{ status, subject, jwt, expiresAt }` persisted via pluggable `SessionStorage` (`"local"` / `"session"` / `"memory"` / custom object). Hydrates on construction; discards expired sessions. Exposes `subscribe` (Svelte-compatible), `get()`, and state-mutation methods (`setAuthenticated`, `setUnverified`, `clear`, `patchSubject`). Writes are driven by `AuthManager`, not consumers directly.
|
|
164
|
+
|
|
165
|
+
- **`suite.auth: AuthManager`** — verbs only, no state. `register` / `login` / `logout` / `resendVerification` / `requestPasswordReset` / `changePassword` / `deleteAccount` / `initiateOAuth` (`mode: "popup" | "redirect"`) / `handleOAuthCallback` (redirect mode). Each call pipes the result into the session and fires an `onIdentityChanged` hook that resets + re-initializes every owner-scoped domain with the fresh context.
|
|
166
|
+
|
|
167
|
+
- **`suite.profile: ProfileManager`** — singleton (one-row) `/me` CRUD. `fetch` / `update` / `listOAuth` / `unlinkOAuth`. Every successful fetch/update patches the session subject in place so consumers reading `suite.session` see email / roles / verification / connections without a second fetch. Update emits `profile:updated`.
|
|
168
|
+
|
|
169
|
+
The session subscribes to its own store and propagates `ctx.jwt` + `ctx.subjectId` into every registered owner-scoped domain automatically — authentication changes propagate without any manual wiring from consumers.
|
|
170
|
+
|
|
171
|
+
### Auth events (emitted on the shared pubsub)
|
|
172
|
+
|
|
173
|
+
- `auth:register` — `{ email, requiresVerification }`
|
|
174
|
+
- `auth:login` — `{ email }`
|
|
175
|
+
- `auth:logout` — `{ subjectId? }`
|
|
176
|
+
- `auth:session:changed` — `{ session: SessionState }`
|
|
177
|
+
- `auth:verification:required` — `{ email }` (fired when `status` transitions to `"unverified"`)
|
|
178
|
+
- `profile:updated` — `{ email }`
|
|
179
|
+
- `oauth:linked` — `{ connection }`
|
|
180
|
+
- `oauth:unlinked` — `{ provider }`
|
|
181
|
+
|
|
182
|
+
### OAuth popup protocol
|
|
183
|
+
|
|
184
|
+
`suite.auth.initiateOAuth(provider, opts)` with `mode: "popup"` opens a popup at the server's `/oauth/{provider}/init` URL and awaits a `postMessage` from the server's callback page (`{ type: "oauth_login_success" | "oauth_link_success" | "oauth_error", ... }`). For `mode: "redirect"` the top window navigates and the app's callback page calls `suite.auth.handleOAuthCallback()` on mount.
|
|
185
|
+
|
|
90
186
|
## State Machine
|
|
91
187
|
|
|
92
188
|
```
|
|
@@ -102,15 +198,23 @@ Triggered by `initialize()`, `refresh()`, `create()`, `update()`, `delete()` on
|
|
|
102
198
|
|
|
103
199
|
1. **Client NEVER sets `owner_id`.** The server stamps it from the authenticated JWT via `@marianmeres/collection`'s `ownerIdExtractor`. Including `owner_id` in a create/update payload will be rejected (belt-and-braces) or silently ignored (immutability guarantee on update).
|
|
104
200
|
|
|
105
|
-
2. **Ownership mismatches return 404, not 403.** When the server's `ownerIdScope` rejects access to a foreign row, it responds 404 to avoid leaking row existence.
|
|
201
|
+
2. **Ownership mismatches return 404, not 403.** When the server's `ownerIdScope` rejects access to a foreign row, it responds 404 to avoid leaking row existence. For `list`/`refresh`, adapters must throw — the manager transitions to `error`. For `getOne`, a throw lands only in a returned `null` (see invariant 7).
|
|
202
|
+
|
|
203
|
+
3. **Row ids default to `model_id`, fallback `id`.** Override via `getRowId` in `OwnsuiteDomainConfig` or `OwnedCollectionManagerOptions` when rows have a different key shape. Empty string is rejected.
|
|
106
204
|
|
|
107
|
-
|
|
205
|
+
4. **`initialize()` never rejects.** Per-domain errors land in that domain's `error` state; the top-level promise resolves. Use `suite.hasErrors()` / `suite.errors()` to detect failed boots, or subscribe to `domain:error`.
|
|
108
206
|
|
|
109
|
-
|
|
207
|
+
5. **Optimistic updates roll back per-row on failure.** `update` mutates the single target row; on error that row reverts to its pre-call value. `delete` removes the target row; on error it is re-inserted at its original position (unless another op has since re-added it). `create` does NOT optimistically insert. Rollback reads the *live* store so an interleaved `refresh()` that brought new rows is preserved.
|
|
110
208
|
|
|
111
|
-
|
|
209
|
+
6. **`OwnsuiteContext.subjectId` is a hint, not authorization.** The server is authoritative. Setting it client-side has no security effect. When subject changes, call `suite.setContext(ctx, { replace: true, refresh: true })` to clear stale per-subject caches.
|
|
112
210
|
|
|
113
|
-
|
|
211
|
+
7. **`getOne()` does NOT transition the domain to `error`.** A failing single-row read (commonly a 404 for an un-owned row) returns `null` and emits nothing. The list state is preserved.
|
|
212
|
+
|
|
213
|
+
8. **`update(id)` for a row absent from the cached list does NOT insert.** A missing index means the row was filtered out or never loaded — the successful server response is acknowledged (`own:row:updated` is emitted) but the list remains untouched. Call `refresh()` to surface the row.
|
|
214
|
+
|
|
215
|
+
9. **Mutations serialize; reads abort-supersede.** Within a single manager, `create/update/delete` run one-at-a-time in call order. A newer `initialize/refresh` aborts an older one (the older call becomes a no-op).
|
|
216
|
+
|
|
217
|
+
10. **`ctx.signal` is present on every adapter call.** Adapters should forward it to `fetch()`. Signals abort on `reset()`, `destroy()`, and read-supersede.
|
|
114
218
|
|
|
115
219
|
## Common Patterns
|
|
116
220
|
|
|
@@ -152,6 +256,29 @@ suite.on("domain:error", (e) => {/* e.error */});
|
|
|
152
256
|
suite.onAny(({ event, data }) => {/* wildcard envelope */});
|
|
153
257
|
```
|
|
154
258
|
|
|
259
|
+
### Detecting boot failures
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
await suite.initialize();
|
|
263
|
+
if (suite.hasErrors()) {
|
|
264
|
+
const errs = suite.errors(); // { [domainName]: DomainError }
|
|
265
|
+
// route to error UI, log, retry, ...
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Switching subject mid-session
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Clears the previous subject's context keys and re-fetches every domain.
|
|
273
|
+
suite.setContext({ subjectId: newId }, { replace: true, refresh: true });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Cleanup
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
suite.destroy(); // aborts in-flight requests, unsubscribes pubsub, drops adapters
|
|
280
|
+
```
|
|
281
|
+
|
|
155
282
|
### Implementing a real adapter
|
|
156
283
|
|
|
157
284
|
```typescript
|
|
@@ -159,14 +286,14 @@ import type { OwnedCollectionAdapter } from "@marianmeres/ownsuite";
|
|
|
159
286
|
import { HTTP_ERROR } from "@marianmeres/http-utils";
|
|
160
287
|
|
|
161
288
|
const adapter: OwnedCollectionAdapter = {
|
|
162
|
-
async list(
|
|
289
|
+
async list(ctx, query) {
|
|
163
290
|
const url = new URL(`/api/shop/me/col/order/mod`, location.origin);
|
|
164
291
|
if (query) for (const [k, v] of Object.entries(query)) url.searchParams.set(k, String(v));
|
|
165
|
-
const res = await fetch(url);
|
|
292
|
+
const res = await fetch(url, { signal: ctx.signal }); // forward abort
|
|
166
293
|
if (!res.ok) throw new HTTP_ERROR.BadRequest(await res.text());
|
|
167
294
|
return await res.json(); // { data, meta }
|
|
168
295
|
},
|
|
169
|
-
// getOne, create, update, delete similarly
|
|
296
|
+
// getOne, create, update, delete similarly — always forward ctx.signal
|
|
170
297
|
};
|
|
171
298
|
```
|
|
172
299
|
|
|
@@ -234,10 +361,16 @@ dev:
|
|
|
234
361
|
## Testing
|
|
235
362
|
|
|
236
363
|
```bash
|
|
237
|
-
deno task test # run all tests (
|
|
364
|
+
deno task test # run all tests (44 tests across 4 files)
|
|
238
365
|
deno task test:watch # watch mode
|
|
239
366
|
```
|
|
240
367
|
|
|
368
|
+
Coverage by file:
|
|
369
|
+
- `tests/ownsuite.test.ts` — core suite + `OwnedCollectionManager` CRUD, events, rollback.
|
|
370
|
+
- `tests/concurrency.test.ts` — critical invariants: concurrent mutations, abort-supersede, `getOne` not setting error, phantom-row prevention, destroy semantics, `errors()`/`hasErrors()` helpers.
|
|
371
|
+
- `tests/auth.test.ts` — `AuthManager` / `ProfileManager` / `SessionManager`: register / login / logout / unverified gate / OAuth login (popup + redirect) / OAuth unlink / profile update patching session / deleteAccount / identity-change hook propagation.
|
|
372
|
+
- `tests/oauth-popup.test.ts` — `openOAuthPopup` message / timeout / popup-closed / origin-mismatch semantics via injectable `PopupWindowHost`.
|
|
373
|
+
|
|
241
374
|
## Build & Publish
|
|
242
375
|
|
|
243
376
|
```bash
|
|
@@ -277,6 +410,58 @@ Joy ships:
|
|
|
277
410
|
|
|
278
411
|
See the full-stack-app-template repo for the end-to-end example.
|
|
279
412
|
|
|
413
|
+
## Breaking changes in 2.0.0
|
|
414
|
+
|
|
415
|
+
The 1.x line has one open set of correctness bugs and a permissive API
|
|
416
|
+
that leaked state into domain errors on non-list operations. 2.0.0 fixes
|
|
417
|
+
those; the behaviors changed are:
|
|
418
|
+
|
|
419
|
+
1. **`getOne()` no longer transitions the domain to `error`.** Previously
|
|
420
|
+
any adapter throw from `getOne` set `state: "error"` on the whole
|
|
421
|
+
domain, invalidating a healthy list view. Now it returns `null` and
|
|
422
|
+
logs at debug level. Callers relying on the error-state transition
|
|
423
|
+
must subscribe differently (wrap `getOne` or inspect adapter errors
|
|
424
|
+
directly).
|
|
425
|
+
|
|
426
|
+
2. **`update(id, ...)` for an id absent from the cached list no longer
|
|
427
|
+
prepends a phantom row** on successful server response. The server
|
|
428
|
+
update is still applied (and `own:row:updated` emitted), but the list
|
|
429
|
+
stays as-is. Call `refresh()` to surface the row. Previously the row
|
|
430
|
+
was inserted at the top of the list.
|
|
431
|
+
|
|
432
|
+
3. **`OwnsuiteContext.signal` is now populated by the manager on every
|
|
433
|
+
adapter call.** Adapters that declared `ctx: OwnsuiteContext` see no
|
|
434
|
+
compile break (the field was already allowed via the index
|
|
435
|
+
signature); adapters that want cancellation should now forward
|
|
436
|
+
`ctx.signal` to `fetch()`. Adapters that ignore it continue to work.
|
|
437
|
+
|
|
438
|
+
4. **`createMockOwnedCollectionAdapter` rejects `create` payloads
|
|
439
|
+
containing `model_id`** by default. Tests that were relying on
|
|
440
|
+
passing a `model_id` at create time must either drop the field or
|
|
441
|
+
opt out via `rejectClientId: false` in the options. Rows with an
|
|
442
|
+
empty-string `model_id` in `seed` are also rejected.
|
|
443
|
+
|
|
444
|
+
5. **Rollback is now per-row, not whole-list.** Behavioral semantics
|
|
445
|
+
are stricter: a failed `update` reverts only the updated row; a
|
|
446
|
+
failed `delete` re-inserts only the deleted row. If your app relied
|
|
447
|
+
on the whole-list-restore side effect (e.g., to drop rows added by
|
|
448
|
+
a concurrent refresh that raced with a failing mutation), note this
|
|
449
|
+
subtle shift.
|
|
450
|
+
|
|
451
|
+
6. **`reset()` now emits `domain:state:changed`** for each domain that
|
|
452
|
+
transitions out of a non-initializing state. Subscribers that count
|
|
453
|
+
events may see more of them.
|
|
454
|
+
|
|
455
|
+
Non-breaking additions: `suite.destroy()`, `suite.errors()`,
|
|
456
|
+
`suite.hasErrors()`, `suite.setContext(ctx, { replace, refresh })`,
|
|
457
|
+
`manager.isDestroyed`, `manager.replaceContext(ctx)`.
|
|
458
|
+
|
|
459
|
+
**Account lifecycle managers (opt-in addition).** `suite.auth` /
|
|
460
|
+
`suite.session` / `suite.profile` attach automatically when
|
|
461
|
+
`adapters.auth` is passed to `createOwnsuite`. Existing owner-scoped
|
|
462
|
+
CRUD consumers see no change unless they opt in. Full surface is
|
|
463
|
+
described in the "Account lifecycle (optional)" section above.
|
|
464
|
+
|
|
280
465
|
## Differences from `@marianmeres/ecsuite`
|
|
281
466
|
|
|
282
467
|
| Aspect | ecsuite | ownsuite |
|