@jskit-ai/agent-docs 0.1.282 → 0.1.284
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/guide/agent/app-extras/assistant.md +14 -2
- package/guide/agent/app-extras/realtime.md +38 -1
- package/guide/agent/app-setup/authentication.md +64 -14
- package/guide/agent/app-setup/console.md +8 -0
- package/guide/agent/app-setup/database-layer.md +23 -103
- package/guide/agent/app-setup/existing-application-migration.md +105 -72
- package/guide/agent/app-setup/multi-homing.md +94 -32
- package/guide/agent/app-setup/quickstart.md +1 -1
- package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
- package/guide/agent/app-setup/upgrading-jskit.md +8 -3
- package/guide/agent/app-setup/users.md +8 -5
- package/guide/agent/framework/application-operations.md +1 -1
- package/guide/agent/framework/crud-operations.md +63 -45
- package/guide/agent/framework/data-access.md +1031 -0
- package/guide/agent/framework/sanity-checks.md +26 -27
- package/guide/agent/framework/typed-features.md +114 -247
- package/guide/agent/index.md +2 -0
- package/package.json +6 -3
- package/patterns/feature-package/PATTERN.md +25 -17
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
- package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/patterns/feature-package/example/variations/types.js +1 -1
- package/patterns/minimal-foundation/example/AGENTS.md +4 -0
- package/patterns/minimal-foundation/example/package.json +4 -4
- package/patterns/minimal-foundation/example/server.js +4 -1
- package/patterns/shell-foundation/example/AGENTS.md +4 -0
- package/patterns/shell-foundation/example/package.json +5 -5
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/server.js +4 -1
- package/reference/autogen/KERNEL_MAP.md +2 -1
- package/reference/autogen/PATTERN_INDEX.md +70 -59
- package/reference/autogen/README.md +1 -1
- package/reference/autogen/packages/agent-docs.md +7 -3
- package/reference/autogen/packages/agent-runtime.md +48 -11
- package/reference/autogen/packages/architecture-inspector.md +6 -2
- package/reference/autogen/packages/assistant-core.md +4 -4
- package/reference/autogen/packages/assistant-runtime.md +33 -13
- package/reference/autogen/packages/auth-provider-local-core.md +3 -1
- package/reference/autogen/packages/auth-provider-local-db-core.md +65 -14
- package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
- package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
- package/reference/autogen/packages/connectors-core.md +22 -2
- package/reference/autogen/packages/console-core.md +9 -5
- package/reference/autogen/packages/crud-core.md +38 -173
- package/reference/autogen/packages/database-runtime.md +8 -32
- package/reference/autogen/packages/http-runtime.md +3 -3
- package/reference/autogen/packages/http-web.md +12 -0
- package/reference/autogen/packages/kernel.md +25 -2
- package/reference/autogen/packages/oidc-issuer-core.md +4 -0
- package/reference/autogen/packages/payments-core.md +29 -6
- package/reference/autogen/packages/realtime.md +15 -6
- package/reference/autogen/packages/resource-core.md +30 -5
- package/reference/autogen/packages/resource-crud-core.md +30 -34
- package/reference/autogen/packages/resource-runtime.md +198 -0
- package/reference/autogen/packages/rewarded-core.md +16 -10
- package/reference/autogen/packages/shell-web.md +1 -0
- package/reference/autogen/packages/users-core.md +26 -14
- package/reference/autogen/packages/workspaces-core.md +17 -10
- package/reference/autogen/tooling/config-eslint.md +69 -0
- package/reference/autogen/tooling/jskit-catalog.md +40 -1
- package/reference/autogen/tooling/jskit-release.md +3 -0
- package/skills/jskit/SKILL.md +4 -1
- package/skills/jskit/references/app-operations.md +1 -1
- package/skills/jskit/references/crud-operations.md +62 -44
- package/skills/jskit/references/data-access.md +1026 -0
- package/skills/jskit/references/database-layer.md +411 -0
- package/skills/jskit/references/existing-application-migration.md +105 -72
- package/skills/jskit/references/heuristics/INDEX.md +6 -4
- package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
- package/skills/jskit/references/heuristics/data-access.md +19 -0
- package/skills/jskit/references/heuristics/filters.md +6 -6
- package/skills/jskit/references/heuristics/row-policies.md +40 -16
- package/skills/jskit/references/heuristics/server-search.md +30 -42
- package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
- package/skills/jskit/references/heuristics/typed-features.md +3 -3
- package/skills/jskit/references/multi-homing.md +94 -32
- package/skills/jskit/references/pattern-index.md +70 -59
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
- package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
- package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +117 -0
- package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +59 -10
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
- package/skills/jskit/references/sanity-checks.md +27 -28
- package/skills/jskit/references/typed-features.md +114 -247
- package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
- package/reference/autogen/packages/json-rest-api-core.md +0 -66
- package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
- package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
- package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
- package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
- /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
- /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
|
-
|
|
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 `
|
|
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
|
|
118
|
-
`
|
|
119
|
-
`
|
|
120
|
-
|
|
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.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
165
|
-
so a waiting registration never keeps the email from its owner.
|
|
166
|
-
new link
|
|
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,35 @@ 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
|
|
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)`, `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)` |
|
|
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. Their types are `AuthLocalAccounts`,
|
|
255
|
+
`AuthLocalSessions` and `AuthLocalRecovery` in
|
|
256
|
+
`@jskit-ai/auth-provider-local-db-core/server/types`.
|
|
207
257
|
|
|
208
258
|
## The first account in production (local auth)
|
|
209
259
|
|
|
@@ -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
|
-
|
|
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](
|
|
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 `
|
|
261
|
-
`
|
|
262
|
-
service, action, permission, JSON API, and route
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
287
|
+
## Writes, versions, locks and deadlocks
|
|
283
288
|
|
|
284
|
-
|
|
285
|
-
`
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
[
|
|
379
|
+
[data access tests](/guide/framework/data-access#tests).
|
|
460
380
|
|
|
461
381
|
## Browser test preparation
|
|
462
382
|
|
|
@@ -101,31 +101,33 @@ application's normal verification script, and Playwright.
|
|
|
101
101
|
|
|
102
102
|
## 3. Choose the narrowest server seam that owns each behavior
|
|
103
103
|
|
|
104
|
-
Conventional resources use `
|
|
105
|
-
`
|
|
106
|
-
permission, action, JSON API, and route mechanics. Extend that contract instead
|
|
104
|
+
Conventional resources use `defineTableResource()`, `defineCrudContract()`
|
|
105
|
+
and `defineCrudFeature()` so JSKIT owns the repeated repository,
|
|
106
|
+
service, permission, action, JSON API, and route mechanics. Extend that contract instead
|
|
107
107
|
of copying it.
|
|
108
108
|
|
|
109
109
|
| Product need | Normal owner |
|
|
110
110
|
| --- | --- |
|
|
111
111
|
| Behavior before, around, or after one standard CRUD operation | `operationLifecycle` |
|
|
112
112
|
| A resource-specific domain method or override | `decorateService` |
|
|
113
|
-
| A resource-specific query, lock, or
|
|
113
|
+
| A resource-specific query, lock, or many-row write | `decorateRepository`, on the table's handle |
|
|
114
114
|
| A named non-CRUD command on the resource | a named CRUD `action` backed by the service |
|
|
115
115
|
| A separate aggregate, workflow, or capability domain | `defineFeature()` with named actions |
|
|
116
|
-
|
|
|
116
|
+
| A report the handle cannot express | one `scopedQuery()` repository method, under `// Knex: <kind> - <reason>` ([data access](/guide/framework/data-access#layer-5-reports-the-resource-cannot-express)) |
|
|
117
117
|
|
|
118
118
|
For mutations, lifecycle phases before commit share one repository
|
|
119
119
|
transaction. An `execute` hook that only normalizes input calls
|
|
120
|
-
`standard(nextInput)` rather than reimplementing the write.
|
|
121
|
-
|
|
120
|
+
`standard(nextInput)` rather than reimplementing the write. The feature that
|
|
121
|
+
owns a table reads and writes it through the handles its `owns` entry gives
|
|
122
|
+
its repository; services, hooks and other features call that repository or
|
|
123
|
+
the owner's API, never the table. External work
|
|
122
124
|
runs after commit, or a durable outbox record is written inside the transaction
|
|
123
125
|
and delivered separately.
|
|
124
126
|
|
|
125
127
|
Give commands product names such as `confirm`, `publish`, or `cancel`, with
|
|
126
128
|
explicit input, permissions, routes where needed, audit behavior, events, and
|
|
127
129
|
tests. Do not hide unrelated workflows behind a generic `execute(anything)`
|
|
128
|
-
action, and do not
|
|
130
|
+
action, and do not write Knex merely because SQL is familiar.
|
|
129
131
|
|
|
130
132
|
### Reuse response serializers
|
|
131
133
|
|
|
@@ -148,9 +150,11 @@ upgrading JSKIT and declaring the directly used
|
|
|
148
150
|
```js
|
|
149
151
|
import Fastify from "fastify";
|
|
150
152
|
import SerializerSelector from "@fastify/fast-json-stringify-compiler";
|
|
151
|
-
import { createCachedResponseSerializerFactory } from "@jskit-ai/kernel/server/http";
|
|
153
|
+
import { createCachedResponseSerializerFactory, resolveTrustedProxies } from "@jskit-ai/kernel/server/http";
|
|
152
154
|
|
|
153
155
|
const app = Fastify({
|
|
156
|
+
// TRUSTED_PROXIES: the proxies whose X-Forwarded-For is believed (loopback without it).
|
|
157
|
+
trustProxy: resolveTrustedProxies(runtimeEnv),
|
|
154
158
|
schemaController: {
|
|
155
159
|
compilersFactory: {
|
|
156
160
|
buildSerializer: createCachedResponseSerializerFactory(SerializerSelector())
|
|
@@ -160,84 +164,113 @@ const app = Fastify({
|
|
|
160
164
|
```
|
|
161
165
|
|
|
162
166
|
Keep the application's other Fastify options, including serializer options and
|
|
163
|
-
validator configuration
|
|
167
|
+
validator configuration, but replace an application's own `trustProxy` setting
|
|
168
|
+
with `resolveTrustedProxies(runtimeEnv)` (`runtimeEnv` is the environment the
|
|
169
|
+
server starts with, `process.env` if it has no other), and its environment
|
|
170
|
+
variable with `TRUSTED_PROXIES`; see
|
|
171
|
+
[sign-in protection](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/authentication#sign-in-protection)
|
|
172
|
+
for the setting. This setup wraps the native default serializer builder;
|
|
164
173
|
retain any deliberately chosen custom compiler instead of replacing or wrapping
|
|
165
174
|
it. Native route-level compiler overrides continue to apply. Update the lockfile
|
|
166
175
|
through the application's normal installation flow and restart the backend to
|
|
167
176
|
activate the new setup. Updating packages or starter examples alone does not
|
|
168
177
|
change an existing application's startup source.
|
|
169
178
|
|
|
170
|
-
###
|
|
179
|
+
### Data access on table resources (json-rest-api 3)
|
|
171
180
|
|
|
172
|
-
Applications using `@jskit-ai/
|
|
181
|
+
Applications using `@jskit-ai/resource-runtime` require Node.js 24 or newer.
|
|
173
182
|
JSKIT's repository and application default is Node.js 26. Upgrade dependencies,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
The standard `defineCrudJsonApiFeature()` API keeps `queryDocuments`,
|
|
178
|
-
`getDocumentById`, `createDocument`, `patchDocumentById`, and
|
|
179
|
-
`deleteDocumentById`, with `{ trx, context }` repository/service options.
|
|
180
|
-
Read, create, and patch services still return tagged JSON:API documents.
|
|
181
|
-
Application-owned hosts use `JsonRestApi` from `json-rest-api`; the JSKIT host
|
|
182
|
-
already owns that setup.
|
|
183
|
-
|
|
184
|
-
For direct resource calls, replace `inputRecord` with exactly one of `data`
|
|
185
|
-
(plain values) or `document` (a JSON:API document). `format` selects output
|
|
186
|
-
independently: `"plain"` or `"jsonapi"`. Replace `simplified` and
|
|
187
|
-
`returnFullRecord` with `format` and `returning: "full" | "minimal" | "none"`.
|
|
188
|
-
The JSKIT host defaults to plain output and full write responses; standard
|
|
189
|
-
document repositories explicitly request JSON:API output and full create/patch
|
|
190
|
-
responses. A plain collection is still `{ data: [...], meta?, links? }`; use
|
|
191
|
-
`extractJsonRestCollectionRows()` when a custom repository needs its rows.
|
|
192
|
-
A query without a page returns only the resource's first 20 rows; a
|
|
193
|
-
server-side read that needs every row uses `queryAllJsonRestCollectionRows()`
|
|
194
|
-
([every row](/guide/framework/typed-features#every-row)).
|
|
183
|
+
resource calls and hooks together; rebuild native database dependencies after
|
|
184
|
+
changing Node.
|
|
195
185
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
data: { title: "Revised title" },
|
|
200
|
-
format: "jsonapi",
|
|
201
|
-
returning: "full",
|
|
202
|
-
transaction: trx
|
|
203
|
-
}, createJsonRestContext(context));
|
|
204
|
-
```
|
|
186
|
+
Every table has one table resource and one owner feature, and every read and
|
|
187
|
+
write goes through that table's handle
|
|
188
|
+
([data access](/guide/framework/data-access)). Rename as you go:
|
|
205
189
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`
|
|
213
|
-
`
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
`
|
|
227
|
-
`
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
190
|
+
| Before | Now |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `defineCrudResource()` | `defineTableResource()` plus `defineCrudContract()` |
|
|
193
|
+
| `defineCrudJsonApiFeature()`, `createCrudResourceRuntime()` | `defineCrudFeature({ crud })` |
|
|
194
|
+
| the CRUD runtime's `listByIds()`, `listByForeignIds()` | `queryAll({ filters: { [key]: ids } })` on a filter declared `{ type: "array", filterOperator: "in" }`; a page's children with `createHooksToCollectChildren()` |
|
|
195
|
+
| the CRUD runtime's lookup hydration | `include` on the CRUD list or view |
|
|
196
|
+
| the CRUD runtime's `virtualFields` | `queryFields` in the `owns` entry, or `decorateService` for a value SQL cannot compute |
|
|
197
|
+
| the CRUD runtime's `createdAt`/`updatedAt` stamping | every handle write stamps them; only a system context sets its own |
|
|
198
|
+
| the CRUD runtime's output validation | the table resource's schema, whose types json-rest-api reads each field as |
|
|
199
|
+
| `defineCrudFeature`'s `ownershipFilter`, `searchSchema` | `visibility`; an `owns` entry |
|
|
200
|
+
| a lifecycle hook's `trx`; a decorator's or CRUD capability's `resource` | `options`; `crud` |
|
|
201
|
+
| `decorateRepository`'s `database`, `http`, `jsonRestApi` | its `repository` and `resources`, each owned table's handle |
|
|
202
|
+
| `@jskit-ai/crud-core/server/fieldAccess`, `/server/lookups` | removed (nothing called them); restrict fields in the CRUD contract and its `decorateService` |
|
|
203
|
+
| `runtime.json-rest-api`, `addResourceIfMissing()`, `createJsonRestResourceScopeOptions()` | `defineFeature({ owns })`; a row policy, search schema or query fields go in the `owns` entry |
|
|
204
|
+
| `queryDocuments()`, `getDocumentById()` | `query({ page: { size } })`, `get(id)` |
|
|
205
|
+
| `queryAllJsonRestCollectionRows()`, `queryEveryDocument()` | `queryAll(params)` |
|
|
206
|
+
| `createDocument()`, `createDocuments()` | `create()`, `createMany()` |
|
|
207
|
+
| `patchDocumentById()`, `patchDocumentIfLockVersion()` | `patch(id, changes, options)`, `patch(id, changes, { ...options, lockVersion })` |
|
|
208
|
+
| `deleteDocumentById()` | `delete(id)` |
|
|
209
|
+
| `getDocumentById(…, { forUpdate: true })`, `applyForUpdate()`, `lockScopedRecordId()` | `lock(id)` inside `withTransaction` |
|
|
210
|
+
| `runInTransaction()`, `createWithTransaction()`, `joinOrOpenTransaction()`, `resolveTransaction()`, `resolveQueryOptions()`, `resolveRepoClient()` | `withTransaction(options, (txOptions) => …)` |
|
|
211
|
+
| `returning: "full" \| "minimal"` | `returning: "record" \| "id"` (`"none"` is unchanged) |
|
|
212
|
+
| a hand-built `visibilityContext` or scope values, `applyVisibility()`, `applyVisibilityOwners()` | the caller's context, or `systemContext(reason).forWorkspace(id)` |
|
|
213
|
+
| a field's `default:` | `defaultTo:` (`default` is refused, since json-rest-api never read it) |
|
|
214
|
+
| a `createdAt` or `updatedAt` default of now | nothing: drop it, since every handle write stamps them |
|
|
215
|
+
| a CRUD contract that showed every column | a contract shows only its `fields`, so list every field the API showed |
|
|
216
|
+
| reading every row | `queryAll`, which refuses above 10,000 rows, so page with `query()` |
|
|
217
|
+
| an app's own wrappers around old calls | the handle method they wrapped, then delete the wrapper |
|
|
218
|
+
|
|
219
|
+
Handles answer records, `{ id, ...fields }`, with a belongs-to field as the
|
|
220
|
+
related id; the CRUD service alone answers JSON:API documents. Every call takes
|
|
221
|
+
`options` last: the caller's context, and inside a transaction the `txOptions`
|
|
222
|
+
`withTransaction` handed its work. A call without a context throws on a table
|
|
223
|
+
that is not public.
|
|
224
|
+
|
|
225
|
+
json-rest-api 3 changes what a failed write means: any failed write inside a
|
|
226
|
+
managed transaction fails the whole transaction. "Insert, catch the duplicate,
|
|
227
|
+
then read" inside a transaction no longer works; use
|
|
228
|
+
`createMany(rows, { ...options, onConflict: { keys, action: "ignore" } })` and then read,
|
|
229
|
+
or read first. A `get`, `patch`, `delete` or `lock` of a missing or hidden
|
|
230
|
+
record throws json-rest-api's not-found error (a 404); with `missing: "skip"`
|
|
231
|
+
it answers `null`, without failing the caller's transaction. Upsert rows carry no
|
|
232
|
+
`id`, never repeat their conflict keys, and name the ownership field among
|
|
233
|
+
those keys; only a table with no owners and no row policy may key the upsert on
|
|
234
|
+
the id alone, `onConflict: { keys: ["id"] }`, with an id on every row. `patchWhere` and `deleteWhere` need filters, or `all: true`.
|
|
231
235
|
|
|
232
236
|
Preserve `error.transactionOutcome` and its cause when translating failures:
|
|
233
237
|
`none` means no transaction was acquired or accepted, `pending` leaves
|
|
234
238
|
completion to the owner, `rolledBack` confirms rollback, `committed` confirms
|
|
235
239
|
commit, and `unknown` means the outcome is unresolved. These describe database
|
|
236
|
-
transaction evidence, not external effects. A failed
|
|
237
|
-
`
|
|
238
|
-
not
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
transaction evidence, not external effects. A failed `afterCommit` hook raises
|
|
241
|
+
`RestApiWriteError` with outcome `committed` and does not undo the write. Do
|
|
242
|
+
not turn pending, committed, or unknown write failures into successful
|
|
243
|
+
missing-record results, or automatically replay a committed or unresolved
|
|
244
|
+
write.
|
|
245
|
+
|
|
246
|
+
### Moving hand-written SQL behind resources
|
|
247
|
+
|
|
248
|
+
An application with Knex in its repositories moves it behind table resources
|
|
249
|
+
one table at a time:
|
|
250
|
+
|
|
251
|
+
1. **Inventory.** List every statement that reads or writes a table: the
|
|
252
|
+
repository methods, raw queries, migrations' seed data, jobs and scripts.
|
|
253
|
+
`jskit check` and the data-access lint rules (`jskit/no-direct-knex`,
|
|
254
|
+
`jskit/no-hand-scope`) find most of them.
|
|
255
|
+
2. **One owner per table.** Pick the feature that owns each table and give the
|
|
256
|
+
table one table resource with its `ownership`. Two features writing one
|
|
257
|
+
table means one of them asks the other.
|
|
258
|
+
3. **Owner APIs.** Code outside the owner calls the owner's service or
|
|
259
|
+
repository capability. JSKIT's own tables already work this way: users
|
|
260
|
+
(`users.profiles`, `users.settings`), workspaces, local sign-in and console
|
|
261
|
+
settings.
|
|
262
|
+
4. **Map each statement to a layer**
|
|
263
|
+
([data access](/guide/framework/data-access)): one record or page →
|
|
264
|
+
Layer 1; screens over HTTP → Layer 2; a loop of writes → a many-row write;
|
|
265
|
+
a `forUpdate` read → a lock; totals and reports → `aggregate`, a window
|
|
266
|
+
include, or one `scopedQuery()` method; work over every workspace → a job
|
|
267
|
+
with `systemScope`.
|
|
268
|
+
5. **Two tests per path.** `expectScoped` proves the path never reads or
|
|
269
|
+
changes a second owner's rows; `expectFlatQueries` proves its statements do
|
|
270
|
+
not grow with its rows. Both come from `@jskit-ai/resource-runtime/test`.
|
|
271
|
+
|
|
272
|
+
Keep each move behaviour-preserving
|
|
273
|
+
([refactors](/guide/framework/sanity-checks#refactors)).
|
|
241
274
|
|
|
242
275
|
### Workspace invitation transaction compatibility (workspaces-core 0.1.198)
|
|
243
276
|
|