@jskit-ai/agent-docs 0.1.283 → 0.1.285

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/guide/agent/app-extras/assistant.md +14 -2
  2. package/guide/agent/app-extras/realtime.md +38 -1
  3. package/guide/agent/app-setup/authentication.md +76 -14
  4. package/guide/agent/app-setup/console.md +8 -0
  5. package/guide/agent/app-setup/database-layer.md +23 -103
  6. package/guide/agent/app-setup/existing-application-migration.md +134 -72
  7. package/guide/agent/app-setup/multi-homing.md +94 -32
  8. package/guide/agent/app-setup/quickstart.md +1 -1
  9. package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
  10. package/guide/agent/app-setup/upgrading-jskit.md +17 -3
  11. package/guide/agent/app-setup/users.md +8 -5
  12. package/guide/agent/framework/application-operations.md +1 -1
  13. package/guide/agent/framework/crud-operations.md +63 -45
  14. package/guide/agent/framework/data-access.md +1207 -0
  15. package/guide/agent/framework/sanity-checks.md +26 -27
  16. package/guide/agent/framework/typed-features.md +117 -248
  17. package/guide/agent/index.md +2 -0
  18. package/package.json +6 -3
  19. package/patterns/feature-package/PATTERN.md +25 -17
  20. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  21. package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  22. package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
  23. package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
  24. package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
  25. package/patterns/feature-package/example/variations/types.js +1 -1
  26. package/patterns/minimal-foundation/example/AGENTS.md +4 -0
  27. package/patterns/minimal-foundation/example/package.json +4 -4
  28. package/patterns/minimal-foundation/example/server.js +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +4 -0
  30. package/patterns/shell-foundation/example/package.json +5 -5
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/server.js +4 -1
  33. package/reference/autogen/KERNEL_MAP.md +3 -1
  34. package/reference/autogen/PATTERN_INDEX.md +70 -59
  35. package/reference/autogen/README.md +1 -1
  36. package/reference/autogen/packages/agent-docs.md +7 -3
  37. package/reference/autogen/packages/agent-runtime.md +48 -11
  38. package/reference/autogen/packages/architecture-inspector.md +6 -2
  39. package/reference/autogen/packages/assistant-core.md +4 -4
  40. package/reference/autogen/packages/assistant-runtime.md +33 -13
  41. package/reference/autogen/packages/auth-provider-local-core.md +3 -1
  42. package/reference/autogen/packages/auth-provider-local-db-core.md +69 -14
  43. package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
  44. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
  45. package/reference/autogen/packages/connectors-core.md +22 -2
  46. package/reference/autogen/packages/console-core.md +9 -5
  47. package/reference/autogen/packages/crud-core.md +45 -175
  48. package/reference/autogen/packages/database-runtime-mysql.md +0 -26
  49. package/reference/autogen/packages/database-runtime.md +11 -33
  50. package/reference/autogen/packages/http-runtime.md +3 -3
  51. package/reference/autogen/packages/http-web.md +12 -0
  52. package/reference/autogen/packages/kernel.md +27 -2
  53. package/reference/autogen/packages/oidc-issuer-core.md +4 -0
  54. package/reference/autogen/packages/payments-core.md +28 -6
  55. package/reference/autogen/packages/realtime.md +15 -6
  56. package/reference/autogen/packages/resource-core.md +32 -5
  57. package/reference/autogen/packages/resource-crud-core.md +30 -34
  58. package/reference/autogen/packages/resource-runtime.md +220 -0
  59. package/reference/autogen/packages/rewarded-core.md +16 -10
  60. package/reference/autogen/packages/shell-web.md +1 -0
  61. package/reference/autogen/packages/users-core.md +33 -14
  62. package/reference/autogen/packages/workspaces-core.md +17 -10
  63. package/reference/autogen/tooling/config-eslint.md +74 -0
  64. package/reference/autogen/tooling/jskit-catalog.md +50 -1
  65. package/reference/autogen/tooling/jskit-release.md +5 -0
  66. package/skills/jskit/SKILL.md +4 -1
  67. package/skills/jskit/references/app-operations.md +1 -1
  68. package/skills/jskit/references/crud-operations.md +62 -44
  69. package/skills/jskit/references/data-access.md +1202 -0
  70. package/skills/jskit/references/database-layer.md +411 -0
  71. package/skills/jskit/references/existing-application-migration.md +134 -72
  72. package/skills/jskit/references/heuristics/INDEX.md +6 -4
  73. package/skills/jskit/references/heuristics/crud-authoring.md +26 -15
  74. package/skills/jskit/references/heuristics/data-access.md +19 -0
  75. package/skills/jskit/references/heuristics/filters.md +6 -6
  76. package/skills/jskit/references/heuristics/row-policies.md +40 -16
  77. package/skills/jskit/references/heuristics/server-search.md +30 -42
  78. package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
  79. package/skills/jskit/references/heuristics/typed-features.md +3 -3
  80. package/skills/jskit/references/multi-homing.md +94 -32
  81. package/skills/jskit/references/pattern-index.md +70 -59
  82. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
  83. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  84. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  88. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
  89. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
  90. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
  91. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
  92. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  93. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  94. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  95. package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
  96. package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
  97. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
  98. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
  99. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
  100. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
  101. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
  102. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
  103. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
  104. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
  105. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
  106. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
  107. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
  108. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
  109. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
  110. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
  111. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
  112. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
  113. package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +120 -0
  114. package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
  115. package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
  116. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  117. package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
  118. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
  119. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  120. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  121. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
  122. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
  123. package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
  124. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
  125. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +63 -10
  126. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
  127. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
  128. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
  131. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
  132. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
  133. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
  134. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
  135. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
  136. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
  137. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
  138. package/skills/jskit/references/sanity-checks.md +27 -28
  139. package/skills/jskit/references/typed-features.md +117 -248
  140. package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
  141. package/reference/autogen/packages/json-rest-api-core.md +0 -66
  142. package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
  143. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
  144. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
  145. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
  146. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
  147. package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
  148. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
  149. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
  150. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
  151. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
  152. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
@@ -124,11 +124,16 @@ above. This section documents that existing surface's behavior and migration
124
124
  constraints, not a second engine/tool implementation.
125
125
 
126
126
  ```bash
127
- npm install @jskit-ai/assistant-runtime
127
+ npm install @jskit-ai/assistant-runtime @jskit-ai/resource-runtime
128
128
  npm run db:migrate
129
129
  ```
130
130
 
131
- These migration instructions apply only to an application selecting the database
131
+ The application registers `AssistantStoreFeature` (from
132
+ `@jskit-ai/assistant-runtime/server`) in its server feature list. It owns the
133
+ assistant's four tables as table resources and provides their repositories as
134
+ `assistant.repositories`, which `AssistantFeature` uses; without it, or an
135
+ `assistant.conversations` runtime, the assistant refuses to start. These
136
+ migration instructions apply only to an application selecting the database
132
137
  integration. Its `assistant/assistant-surface` pattern composes the SQL surface;
133
138
  there is no assistant generator.
134
139
 
@@ -205,6 +210,13 @@ user change the instructions; choose it only deliberately. A malformed value
205
210
  stops the server from starting. Anyone else is refused with
206
211
  `assistant.runtime.settings.updateForbidden` (403).
207
212
 
213
+ The settings the server returns say whether the signed-in person may change
214
+ them (`canUpdate`, from the same check that refuses the change), and the
215
+ settings pane follows it: people who may change them get **Save** in the app
216
+ bar, anyone else sees the instructions read-only, without Save. The browser
217
+ never guesses the permission, so a `settingsUpdatePermission` in server config
218
+ needs nothing on the client.
219
+
208
220
  ### Existing SQL conversations
209
221
 
210
222
  The optional database integration uses the common conversation runtime for
@@ -29,7 +29,44 @@ The `event_scope` audience, the default for action entity-change and CRUD
29
29
  example one written from a public route or by a job, reaches nobody and logs a
30
30
  warning; `event_scope` never widens to all users. Broadcast to every signed-in
31
31
  user only through the explicit `all_users` audience; a CRUD feature selects it
32
- with `defineCrudJsonApiFeature({ realtimeAudience: "all_users" })`.
32
+ with `defineCrudFeature({ realtimeAudience: "all_users" })`.
33
+
34
+ An audience entry may also reach users that only a record's owner knows, such
35
+ as the person an invitation was sent to. It provides
36
+ `audienceUsers({ event, options })`, which answers, or resolves to, a list of
37
+ user ids; realtime adds each one's user room. It asks the owners' APIs (for
38
+ example `activeMemberIds(workspaceId, options)` of `workspaces.memberships`, or
39
+ the record owner's repository) and never the database directly. It runs once
40
+ for the event, however many sockets receive it, and `options.context` is the
41
+ event's own scope (its workspace, user, or user in a workspace), so the owners
42
+ answer for that scope and not for a request's. An event written on a customer's
43
+ or a public surface has a global scope, which reaches no room by itself: the
44
+ framework adds the workspace the request resolved as its `workspaceId`, so the
45
+ audience reads there and `all_workspace_users` reaches it. The payload never
46
+ chooses the scope, since a payload may copy an input id: an `audienceUsers`
47
+ reads with `options`, never with a workspace the payload names. An event with
48
+ no workspace and no user reads with no context. Realtime needs no database, so
49
+ those contexts come from `RealtimeAudienceScopeFeature`
50
+ (`@jskit-ai/realtime/server/RealtimeAudienceScopeFeature`): an application whose
51
+ action events declare such an audience registers it beside its other features,
52
+ or realtime refuses to boot and names the actions. An audience that reads only
53
+ public tables (users, workspaces, memberships, invitations) says so with
54
+ `publicTablesOnly: true` and reads with no context; an owned table would throw
55
+ `MissingScopeError` there. An audience given as a function of the execution is
56
+ not known at boot, so realtime checks it when the event is published: without
57
+ the feature, an entry that reads in the event's scope is logged as an error and
58
+ skipped. An `audienceUsers` that fails is logged as an error and skipped too,
59
+ and the event still reaches the audience's other rooms:
60
+
61
+ ```js
62
+ realtime: {
63
+ event: "users.bootstrap.changed",
64
+ audience: {
65
+ preset: "event_scope",
66
+ audienceUsers: ({ event, options }) => invitations.recipientUserIds(event.entityId, options)
67
+ }
68
+ }
69
+ ```
33
70
 
34
71
  Client features register listeners through `@jskit-ai/realtime`. Keep query
35
72
  invalidation close to the resource that owns the query keys.
@@ -114,10 +114,20 @@ A route name auth does not have, or a value that is neither
114
114
  or text such as `"30 seconds"` or `"1 hour"`.
115
115
 
116
116
  The address is Fastify's `request.ip`. Behind a reverse proxy every request
117
- comes from the proxy, so all clients would share one count: create Fastify with
118
- `trustProxy` set to the proxy's address (for example
119
- `Fastify({ trustProxy: "127.0.0.1" })`), and never to `true` when clients can
120
- also reach the server directly.
117
+ comes from the proxy, so all clients would share one count. The app's
118
+ `server.js` creates Fastify with
119
+ `trustProxy: resolveTrustedProxies(runtimeEnv)` (from
120
+ `@jskit-ai/kernel/server/http`), which reads one setting, `TRUSTED_PROXIES`:
121
+ the addresses or ranges of the proxies in front of the server, comma-separated,
122
+ in Fastify's `trustProxy` syntax (`10.0.0.5`, `10.0.0.0/24`, `fd00::/8`, or
123
+ `loopback`, `linklocal` or `uniquelocal`), for example
124
+ `TRUSTED_PROXIES=loopback,10.0.0.5`. Only those proxies' `X-Forwarded-For` (and
125
+ `-Host` and `-Proto`) is believed, so each client behind them has its own count
126
+ and a client that sends the header itself changes nothing. Without the setting
127
+ only a proxy on the same machine (`loopback`) is believed. `true` and hop counts
128
+ stop startup, because they believe a header any client can send. Name the
129
+ narrowest addresses that are only your proxies: a private range shared with
130
+ other machines lets each of them choose its address.
121
131
 
122
132
  **Per account (local auth).** After 5 failed sign-ins in a row for one email,
123
133
  that email must wait 30 seconds before its next attempt; each further failure
@@ -154,23 +164,35 @@ password, in every environment, unless `AUTH_LOCAL_EMAIL_CONFIRMATION=false`.
154
164
  Registering keeps the registration waiting, with no account, profile or
155
165
  session, and answers `requiresEmailConfirmation: true`; the sign-in page then
156
166
  shows "Confirm your email", with a button to send a new link and a field for
157
- the link or its code. Opening the link (`/auth/login?confirmation_token=...`)
158
- creates the account and signs the person in. The `auth.local.user.registered`
159
- hook runs then, not on registering. Signing in with the right password while the registration waits
160
- is refused with status 403 and `auth.login.emailUnconfirmed`, and the sign-in
161
- page shows how to confirm; a wrong password is an ordinary failed sign-in.
167
+ the link or its code. Confirming takes the link's code and the password the
168
+ registration was made with (`POST /api/register/confirm` with
169
+ `{ token, password }`): opening the link (`/auth/login?confirmation_token=...`)
170
+ asks for that password, and right after registering the page keeps the one
171
+ just typed. Confirming creates the account and signs the person in. The
172
+ `auth.local.user.registered` hook runs then, not on registering. A missing or
173
+ wrong password creates nothing and keeps the registration and its link, with
174
+ status 400 and `auth.local.register.confirmationPasswordInvalid`. Signing in
175
+ with the right password while the registration waits is refused with status
176
+ 403 and `auth.login.emailUnconfirmed`, and the sign-in page shows how to
177
+ confirm; a wrong password is an ordinary failed sign-in.
162
178
 
163
179
  A link's code is random, kept only as a hash, works once and expires after a
164
- day. Registering the same email again, or asking for a new link, replaces it,
165
- so a waiting registration never keeps the email from its owner. Asking for a
166
- new link answers the same whether or not a registration waits.
180
+ day. Registering the same email again replaces the waiting registration whole
181
+ and its link, so a waiting registration never keeps the email from its owner.
182
+ Asking for a new link replaces the link and keeps the registration, password
183
+ included; it answers the same whether or not a registration waits, and sends
184
+ nothing for an expired one. The link proves the email and the password proves
185
+ the registration: whoever registers an email, before its owner or after, the
186
+ account its link creates has the password of the person who confirms it. A
187
+ link its owner never asked for confirms nothing with the owner's password;
188
+ the owner registers instead.
167
189
 
168
190
  The link is emailed by the optional `auth.local.confirmation-sender`
169
191
  capability, an async function accepting `{ email, confirmationUrl }`, built
170
192
  like the recovery sender below; with it, `APP_PUBLIC_URL` is required and the
171
193
  link is never returned or logged. In development without one, the link is in
172
194
  the registration's answer (`confirmationUrl`) and the log, and the sign-in page
173
- shows it with a button that confirms it. In production (`NODE_ENV=production`)
195
+ shows it with a button that confirms it with the password typed. In production (`NODE_ENV=production`)
174
196
  without one, while anyone may register, the server refuses to start, naming
175
197
  the ways out: install a sender, set `AUTH_LOCAL_EMAIL_CONFIRMATION=false`, or
176
198
  set `AUTH_LOCAL_SELF_REGISTRATION=false`. For direct library
@@ -203,7 +225,47 @@ upgrade wait. Waiting registrations are kept in the file store
203
225
  the `auth_local_registrations` table (run `npm run db:migrate`). A custom
204
226
  `auth.local.backend` implements the `registrations` collection. Tests that
205
227
  register and expect a session either confirm with the answer's
206
- `confirmationUrl` or set `AUTH_LOCAL_EMAIL_CONFIRMATION=false`.
228
+ `confirmationUrl` and the password they registered with, or set
229
+ `AUTH_LOCAL_EMAIL_CONFIRMATION=false`.
230
+
231
+ ## Local accounts from application code (database backend)
232
+
233
+ Application code that works with local auth's records beside local auth, such
234
+ as an emailed sign-in link or a job that removes old sessions, requires their
235
+ owners' capabilities from `@jskit-ai/auth-provider-local-db-core` and never
236
+ reads or writes the `auth_local_*` tables. Every method takes the call's
237
+ `options` last and runs in `options.trx` when there is one. That transaction
238
+ must be a managed one: open it with one of the application's own table
239
+ resource handles, `handle.withTransaction(options, (txOptions) => …)`, and pass
240
+ `txOptions` on. Their writes refuse a transaction from `knex.transaction()`, and
241
+ a failed write fails the whole transaction even when the work catches its
242
+ error.
243
+
244
+ | Capability | Methods |
245
+ | --- | --- |
246
+ | `auth.local.accounts` | `create(account, options)`, `findById(userId, options)`, `findByEmail(email, options)`, `updatePassword(userId, passwordRecord, options)`, `updateProfile(userId, { displayName }, options)`, `markEmailVerified(userId, options)` |
247
+ | `auth.local.sessions` | `create(session, options)`, `update(sessionId, { purpose, expiresAt }, { ...options, onlyIf })`, `revoke(sessionId, options)`, `revokeForUser(userId, { ...options, exceptSessionId })`, `deleteExpired(now, options)` |
248
+ | `auth.local.recovery` | `create(token, options)`, `findByTokenHash(tokenHash, options)`, `consume(tokenId, options)`, `consumeForUser(userId, { ...options, idPrefix })` |
249
+
250
+ `markEmailVerified` records that the account proved it owns its email address,
251
+ for example by using an emailed link, in the `email_verified_at` column (run
252
+ `npm run db:migrate`); the first proof is kept, and the account answers it as
253
+ `emailVerifiedAt` (empty until then). `deleteExpired` deletes the sessions that
254
+ expired by `now` and answers how many.
255
+
256
+ A recovery token's kind is the start of its id: local sign-in's password resets
257
+ are `rec_…`, and an app's own kind (a sign-in link, say) takes a prefix of its
258
+ own, such as `magic_…`. `consumeForUser` consumes the user's open tokens of the
259
+ kind `idPrefix` names and no others, so a new sign-in link leaves an open
260
+ password reset usable, and a completed reset leaves sign-in links open.
261
+ `update` with `onlyIf` (`userId`, `purpose`, `revokedAt`) changes the session
262
+ only while it still has those values, compared in the UPDATE itself, and
263
+ answers null, writing nothing, when it does not: promote a sign-in link's
264
+ session with
265
+ `update(sessionId, { purpose: "normal", expiresAt }, { ...options, onlyIf: { userId, purpose: "recovery", revokedAt: null } })`
266
+ and treat null as a refused link. Their types are `AuthLocalAccounts`,
267
+ `AuthLocalSessions` and `AuthLocalRecovery` in
268
+ `@jskit-ai/auth-provider-local-db-core/server/types`.
207
269
 
208
270
  ## The first account in production (local auth)
209
271
 
@@ -33,6 +33,14 @@ Do not create a console merely to host one ordinary settings page.
33
33
  The first-owner policy, if the product selects it, is a runtime product rule;
34
34
  it is not a hard-coded seed account.
35
35
 
36
+ Server code that needs the console's settings, such as who owns the console,
37
+ requires `console.settings` and reads them with `getSingleton(options)`, which
38
+ answers `{ id, ownerUserId, settings, createdAt, updatedAt }`; it never reads
39
+ the `console_settings` table. Its type is `ConsoleSettings` in
40
+ `@jskit-ai/console-core/server/types`. To check the console's owner, use
41
+ `isConsoleOwnerUserId(userId, options)` of `console.core`'s
42
+ `services.access`.
43
+
36
44
  ## Verification
37
45
 
38
46
  Test authorized and forbidden direct entry, first-owner behavior, switching,
@@ -12,16 +12,17 @@ genuinely chooses a driver at runtime.
12
12
  For MySQL or MariaDB:
13
13
 
14
14
  ```bash
15
- npm install @jskit-ai/database-runtime-mysql
15
+ npm install @jskit-ai/database-runtime @jskit-ai/database-runtime-mysql
16
16
  ```
17
17
 
18
18
  For PostgreSQL:
19
19
 
20
20
  ```bash
21
- npm install @jskit-ai/database-runtime-postgres
21
+ npm install @jskit-ai/database-runtime @jskit-ai/database-runtime-postgres
22
22
  ```
23
23
 
24
- Each driver brings `@jskit-ai/database-runtime` and the appropriate Knex
24
+ The application declares `@jskit-ai/database-runtime` itself: each driver takes
25
+ it as a peer, so the application holds its one copy, and brings its own Knex
25
26
  driver through ordinary npm dependencies. There is no JSKIT install wizard.
26
27
 
27
28
  ## Connection environment
@@ -47,7 +48,7 @@ credentials. JSKIT never guesses or commits them.
47
48
 
48
49
  The running database provider registers a `database` health check that runs
49
50
  `select 1` within two seconds. The foundations' `GET /api/health` answers `503`
50
- while it fails; see [Health endpoint](./initial-scaffolding.md#health-endpoint).
51
+ while it fails; see [Health endpoint](/guide/app-setup/initial-scaffolding#health-endpoint).
51
52
 
52
53
  ## Migration configuration
53
54
 
@@ -162,7 +163,7 @@ pending migrations in this order, using the default `migrationsDirectory`:
162
163
  2. Application `migrations/`.
163
164
  3. The `constraints/` subdirectory of each discovered package migration
164
165
  directory that has one, in the same order.
165
- 4. Application `migrations/constraints/`.
166
+ 4. Application `migrations/constraints/`, when it exists.
166
167
 
167
168
  Knex sorts filenames within each directory, not across the whole graph; see
168
169
  its [`sortDirsSeparately` option](https://knexjs.org/guide/migrations.html#migration-api).
@@ -257,10 +258,10 @@ database allocation, credentials, lifetime, and environment injection.
257
258
 
258
259
  ## Resource services and custom operations
259
260
 
260
- Conventional persisted resources use `defineCrudResource()` and
261
- `defineCrudJsonApiFeature()` so the framework owns repeated repository,
262
- service, action, permission, JSON API, and route mechanics. This does not make
263
- product CRUD behavior fixed.
261
+ Conventional persisted resources use `defineTableResource()`,
262
+ `defineCrudContract()` and `defineCrudFeature()` so the framework owns
263
+ repeated table ownership, service, action, permission, JSON API, and route
264
+ mechanics. This does not make product CRUD behavior fixed.
264
265
 
265
266
  - `decorateRepository` adds resource-specific queries, locks, or writes.
266
267
  - `decorateService` overrides a standard method or adds domain methods such as
@@ -268,108 +269,27 @@ product CRUD behavior fixed.
268
269
  - `operationLifecycle` surrounds a standard operation with `before`, `execute`,
269
270
  `after`, and mutation-only `afterCommit` phases. Create, update, and delete
270
271
  phases before commit share one repository transaction, and `execute` receives
271
- `standard(nextInput)` for retaining the normal framework write.
272
+ `standard(nextInput)` for retaining the normal framework write. A deadlock
273
+ runs that transaction again, so `before`, `execute` and `after` can run twice
274
+ and touch only the database; an outside effect (an email, an HTTP call) goes
275
+ in `afterCommit`, which runs once after commit, or in an outbox row.
272
276
  - Named `actions` expose non-CRUD service methods through normal input,
273
277
  permission, event, and optional HTTP route contracts.
274
278
 
275
- Repositories own database access. Services and lifecycle hooks orchestrate
276
- repositories. External delivery belongs after commit; when it must be durable,
279
+ The feature that owns a table reads and writes it through the handles its
280
+ `owns` entry gives its repository; services, hooks and other features call
281
+ that repository or the owner's API, never the table. External delivery belongs after commit; when it must be durable,
277
282
  write an outbox record inside the transaction and deliver it separately.
278
283
  A separate Feature is warranted when an operation belongs to another domain,
279
284
  not merely because a useful resource has behavior beyond list and save. Write
280
285
  it as a [typed feature](/guide/framework/typed-features).
281
286
 
282
- ## Writes that return less
287
+ ## Writes, versions, locks and deadlocks
283
288
 
284
- A CRUD repository's `createDocument(payload, options)` and
285
- `patchDocumentById(id, patch, options)` return the full document by default:
286
- the write is followed by a get through the resource's row policy and its
287
- lookups, about four queries and a policy run per linked type. Server code that
288
- does not use the result passes `returning: "minimal"`, which answers
289
- `{ data: { type, id } }`, or `"none"`, which answers `undefined`; a missing or
290
- out-of-scope record still patches to `null`. `createDocuments(payloads,
291
- { trx, context, returning })` creates several records in order in one
292
- transaction, the caller's `trx` or one it owns, so they are all created or
293
- none; its `returning` is `"minimal"` unless the caller asks for `"full"`, and
294
- its results follow the payloads. Each record is still validated and inserted
295
- by itself: a batch saves the read-backs and the transactions, not the inserts.
296
-
297
- ## Optimistic locking
298
-
299
- A CRUD resource whose records carry a version number names it with
300
- `lockVersionField`, an integer schema field over a non-null integer column
301
- with a starting value, such as `0`:
302
-
303
- ```js
304
- defineCrudResource({
305
- namespace: "contracts",
306
- tableName: "contracts",
307
- autofilter: "workspace",
308
- lockVersionField: "lockVersion",
309
- schema: {
310
- // ...
311
- lockVersion: { type: "integer", min: 0, readOnly: true, column: "lock_version" }
312
- }
313
- });
314
- ```
315
-
316
- The CRUD repository's
317
- `patchDocumentIfLockVersion(recordId, expectedLockVersion, patch, { trx, context })`
318
- then applies `patch` only while the record still holds `expectedLockVersion`, a
319
- non-negative integer, and adds one to the lock version in the same
320
- transaction: the caller's `trx`, or one it owns. It returns the patched document, which carries the new lock
321
- version, or `null` when the record does not exist or is outside the caller's
322
- scope. A record whose lock version has moved on rejects with a 409 coded
323
- `crud.document.lockVersionConflict` ("This record changed. Reload and try
324
- again."), and nothing is written. The patch leaves the lock field out; an app
325
- that wants its own wording for the conflict catches that code. A resource
326
- without `lockVersionField` cannot call it, and `defineCrudResource()` rejects
327
- a `lockVersionField` that names no integer schema field.
328
-
329
- ## Row locks
330
-
331
- A check-then-write that must not race another request (is the drier empty?
332
- then load it) reads the record with
333
- `getDocumentById(recordId, query, { trx, context, forUpdate: true })` inside
334
- `withTransaction`. It locks the record's row in that transaction before
335
- reading it, so another `forUpdate` read or a write of the record waits until
336
- the transaction ends, and the read sees the latest version on PostgreSQL; on
337
- MySQL and MariaDB it does when the lock is the transaction's first read of
338
- that row. Without a transaction `forUpdate` throws, and a value other than
339
- `true` or `false` throws too. SQLite runs one transaction at a time on its
340
- single connection, so there is no row lock to take. The CRUD service's
341
- `getDocumentById` passes `forUpdate` through.
342
-
343
- ## Deadlock retries
344
-
345
- When the database aborts a transaction as a deadlock victim (MySQL and MariaDB
346
- `ER_LOCK_DEADLOCK`, errno 1213; PostgreSQL `40P01`), it rolls the whole
347
- transaction back, so JSKIT runs the whole transaction callback again, up to
348
- three more times with a short random pause. The person sees one successful
349
- request instead of a 500. This applies to `runInTransaction()` and
350
- `createWithTransaction()` from `@jskit-ai/database-runtime/shared`, to
351
- `transactionManager.inTransaction()`, to the seed transaction, to every JSON
352
- REST repository's `withTransaction()` (CRUD, users and workspaces), and to a
353
- CRUD `createDocument`, `patchDocumentById`, `patchDocumentIfLockVersion` or
354
- `deleteDocumentById` given no `trx`, which owns its own transaction. A lock wait timeout (errno 1205) is not
355
- retried.
356
-
357
- Only the outermost transaction retries. A call given an existing `trx`, or a
358
- `runInTransaction()` on a transaction, which opens a savepoint, fails at once
359
- and leaves the retry to the transaction that owns it.
360
-
361
- A retried callback must be safe to run again: keep HTTP calls, email, file
362
- uploads and other external effects out of it, before or after the transaction
363
- or in an outbox, as above. CRUD lifecycle `before`, `execute` and `after`
364
- phases are inside the transaction; `afterCommit` is not. A transaction whose
365
- callback cannot be repeated passes `{ retries: 0 }`:
366
- `runInTransaction(knex, work, { retries: 0 })`.
367
-
368
- Application code that opens transactions directly uses the same behavior through
369
- `runInTransaction(knex, work)` instead of `knex.transaction(work)`, or wraps any
370
- other transaction opener in `retryDeadlocks(() => open(work))`. `isDeadlockError(error)`
371
- follows `error.cause` and is false for an error that reports
372
- `transactionOutcome: "committed"`, because that transaction must not run again.
289
+ Writes that skip their read-back (`returning`), optimistic locking
290
+ (`lockVersionField`), row locks and deadlock retries belong to the table
291
+ resource's handle: see [Data access](/guide/framework/data-access), Layers 1
292
+ and 4 and [Transactions](/guide/framework/data-access#transactions).
373
293
 
374
294
  ## Verification
375
295
 
@@ -456,7 +376,7 @@ Endpoints and batch operations get their own database tests at realistic row
456
376
  counts that compare their query counts, counted with `countQueries()` from
457
377
  `@jskit-ai/database-runtime/server/testDatabases`, or `countRuntimeQueries()`
458
378
  for a route of the server the test boots; see the typed features guide's
459
- [query counts at realistic sizes](/guide/framework/typed-features#query-counts-at-realistic-sizes).
379
+ [data access tests](/guide/framework/data-access#tests).
460
380
 
461
381
  ## Browser test preparation
462
382