@jskit-ai/agent-docs 0.1.292 → 0.1.294
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/realtime.md +14 -4
- package/guide/agent/app-setup/authentication.md +19 -8
- package/guide/agent/app-setup/multi-homing.md +31 -3
- package/guide/agent/app-setup/upgrading-jskit.md +4 -0
- package/guide/agent/framework/client-features.md +54 -1
- package/guide/agent/framework/crud-operations.md +7 -0
- package/guide/agent/framework/data-access.md +75 -9
- package/guide/agent/framework/sanity-checks.md +4 -4
- package/guide/agent/framework/typed-features.md +45 -5
- package/package.json +1 -1
- package/patterns/feature-package/example/booking-engine/package.json +2 -2
- package/patterns/minimal-foundation/example/AGENTS.md +5 -1
- package/patterns/minimal-foundation/example/package.json +5 -5
- package/patterns/shell-foundation/example/AGENTS.md +5 -1
- package/patterns/shell-foundation/example/package.json +6 -6
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/reference/autogen/PATTERN_INDEX.md +38 -38
- package/reference/autogen/packages/agent.md +4 -2
- package/reference/autogen/packages/assistant-core.md +1 -1
- package/reference/autogen/packages/assistant-runtime.md +1 -0
- package/reference/autogen/packages/crud-core.md +2 -0
- package/reference/autogen/packages/http-web.md +9 -6
- package/reference/autogen/packages/kernel.md +7 -1
- package/reference/autogen/packages/realtime.md +1 -0
- package/reference/autogen/packages/resource-runtime.md +1 -0
- package/reference/autogen/packages/users-core.md +3 -0
- package/reference/autogen/packages/workspaces-core.md +19 -4
- package/reference/autogen/packages/workspaces-web.md +2 -1
- package/reference/autogen/tooling/config-eslint.md +44 -1
- package/reference/autogen/tooling/jskit-catalog.md +35 -13
- package/skills/jskit/references/client-features.md +54 -1
- package/skills/jskit/references/crud-operations.md +7 -0
- package/skills/jskit/references/data-access.md +75 -9
- package/skills/jskit/references/heuristics/INDEX.md +1 -1
- package/skills/jskit/references/heuristics/data-access.md +1 -0
- package/skills/jskit/references/heuristics/filters.md +14 -0
- package/skills/jskit/references/heuristics/server-search.md +2 -0
- package/skills/jskit/references/multi-homing.md +31 -3
- package/skills/jskit/references/pattern-index.md +38 -38
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +5 -1
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +5 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +5 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +6 -6
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -3
- 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-feature-package/example/packages/books/package.json +3 -3
- package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +12 -1
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +2 -2
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +7 -0
- package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +7 -3
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +3 -3
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +6 -6
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +69 -17
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -0
- package/skills/jskit/references/sanity-checks.md +4 -4
- package/skills/jskit/references/typed-features.md +45 -5
|
@@ -103,10 +103,13 @@ audience contributor is registered and a target room is a user or workspace
|
|
|
103
103
|
room, when its actor's contributed rooms include a target room or it has not
|
|
104
104
|
signed in yet. A socket joins only its own actor's contributed rooms, so a
|
|
105
105
|
publish to `workspace:7` checks that workspace's members' sockets, not every
|
|
106
|
-
connected one. To decide, the publish
|
|
107
|
-
signed-in socket
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
connected one. To decide, the publish asks each contributor once about the
|
|
107
|
+
actors of every signed-in socket outside the target rooms, whatever their
|
|
108
|
+
number, naming only the target rooms' scopes, so a contributor reads only
|
|
109
|
+
those (a publish to `workspace:7` reads workspace 7's memberships among them,
|
|
110
|
+
and a publish to user rooms reads no memberships); meanwhile it looks up the
|
|
111
|
+
contributed rooms of the actors already inside, for their checks. Other
|
|
112
|
+
sockets are not checked for that publish; the 30-second check reaches them. Sockets whose authentication requests are identical, such as tabs of
|
|
110
113
|
one browser, share one `authenticateRequest()` call within a publish or a
|
|
111
114
|
30-second pass, and the sockets a publish or pass checks reuse that one lookup
|
|
112
115
|
of their contributed rooms; nothing is reused from one publish or pass to the
|
|
@@ -140,12 +143,19 @@ them in one query rather than one per actor: it returns, or resolves to, a `Map`
|
|
|
140
143
|
from actor id to that actor's event scopes (`{ kind: "user", id }`,
|
|
141
144
|
`{ kind: "workspace", id }` or `{ kind: "workspace_user", workspaceId, userId }`);
|
|
142
145
|
an actor it leaves out has none. A single actor is asked as a list of one.
|
|
146
|
+
A publish asking whether actors outside its rooms belong to them calls
|
|
147
|
+
`resolveActorsScopes(actorIds, { scopes })` with those rooms' scopes: the
|
|
148
|
+
contributor may read only those scopes, and realtime keeps only them of its
|
|
149
|
+
answer, so a contributor that ignores `scopes` still answers correctly.
|
|
143
150
|
Realtime turns each scope into the same room an `event_scope` audience targets. Sockets
|
|
144
151
|
join those rooms on connection and, on every revalidation, leave the contributed
|
|
145
152
|
rooms the actor no longer belongs to; rooms joined by anything else are left
|
|
146
153
|
alone. `@jskit-ai/workspaces-core` contributes the actors' active workspace
|
|
147
154
|
memberships this way when realtime is installed, with
|
|
148
155
|
`listActiveWorkspaceIdsByUserIds()`: one memberships query per 1,000 actors.
|
|
156
|
+
Asked about some scopes, it reads only the memberships of the workspaces they
|
|
157
|
+
name, with `listActiveWorkspaceIdsByUserIdsAmong()`, and none when they name
|
|
158
|
+
no workspace.
|
|
149
159
|
|
|
150
160
|
The authentication request includes parsed cookies, the `Host`, `Origin` and
|
|
151
161
|
`X-Forwarded-Proto` headers when present, and the socket address and TLS state.
|
|
@@ -161,8 +161,15 @@ The login page shows either refusal, translated, with the wait.
|
|
|
161
161
|
|
|
162
162
|
Local auth confirms the email address of each account it registers by
|
|
163
163
|
password, in every environment, unless `AUTH_LOCAL_EMAIL_CONFIRMATION=false`.
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
Two registrations whose email is already proven sign in at once instead: one
|
|
165
|
+
through an invitation that a feature's invitation context resolver (such as
|
|
166
|
+
workspace invites) resolves, still pending, for the registering email (letter
|
|
167
|
+
case aside) and before its `expiresAt`, since its link reached that address;
|
|
168
|
+
and the first account a production server's setup code admits (see below). An
|
|
169
|
+
invitation passed through as given, with no resolver installed, or whose
|
|
170
|
+
context names no email, proves nothing. Any other registration keeps waiting,
|
|
171
|
+
with no account, profile or session, and answers
|
|
172
|
+
`requiresEmailConfirmation: true`; the sign-in page then
|
|
166
173
|
shows "Confirm your email", with a button to send a new link and a field for
|
|
167
174
|
the link or its code. Confirming takes the link's code and the password the
|
|
168
175
|
registration was made with (`POST /api/register/confirm` with
|
|
@@ -174,7 +181,9 @@ wrong password creates nothing and keeps the registration and its link, with
|
|
|
174
181
|
status 400 and `auth.local.register.confirmationPasswordInvalid`. Signing in
|
|
175
182
|
with the right password while the registration waits is refused with status
|
|
176
183
|
403 and `auth.login.emailUnconfirmed`, and the sign-in page shows how to
|
|
177
|
-
confirm; a wrong password is an ordinary failed sign-in.
|
|
184
|
+
confirm; a wrong password is an ordinary failed sign-in. The refusal holds
|
|
185
|
+
even after `AUTH_LOCAL_EMAIL_CONFIRMATION=false`, which offers no
|
|
186
|
+
confirming: the person registers again.
|
|
178
187
|
|
|
179
188
|
A link's code is random, kept only as a hash, works once and expires after a
|
|
180
189
|
day. Registering the same email again replaces the waiting registration whole
|
|
@@ -210,9 +219,10 @@ Confirmation guards self-registration. An app whose accounts are only invited,
|
|
|
210
219
|
or created through the first account, sets `AUTH_LOCAL_SELF_REGISTRATION=false`
|
|
211
220
|
and needs no confirmation sender: an invitation counts when a feature's
|
|
212
221
|
invitation context resolver (such as workspace invites) resolves it for that
|
|
213
|
-
email, and the first account is admitted by its setup code.
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
email, and the first account is admitted by its setup code. Those
|
|
223
|
+
registrations sign in at once, with or without a sender, except an invitation
|
|
224
|
+
whose resolver names no email, which confirms when a sender is installed. An
|
|
225
|
+
app that refused the register routes itself, with a 404 hook,
|
|
216
226
|
can drop the hook for this setting. The sign-in page then shows its Register
|
|
217
227
|
tab only to an invited person (an `invitationToken` in its address) and, while
|
|
218
228
|
the app has no account, to the first one: local auth reports
|
|
@@ -289,8 +299,9 @@ field (capability `features.firstAccountSetupCode`), and the register body
|
|
|
289
299
|
takes `setupCode`; letter case, spaces and dashes do not matter. A missing code
|
|
290
300
|
is refused with 403 `auth.local.setup.codeRequired`, a wrong one with 403
|
|
291
301
|
`auth.local.setup.codeInvalid`. An OAuth sign-in cannot create that first
|
|
292
|
-
account. The registration then follows the ordinary rules,
|
|
293
|
-
|
|
302
|
+
account. The registration then follows the ordinary rules, except that it
|
|
303
|
+
needs no email confirmation: only the operator can read the code, so it signs
|
|
304
|
+
the owner in at once. The code admits registrations
|
|
294
305
|
until an account exists; then it stops working and its file is deleted, and
|
|
295
306
|
registration needs no code again. Restarting before then makes a new code and
|
|
296
307
|
the old one stops working: the latest log line and the file always agree, and
|
|
@@ -196,12 +196,21 @@ method takes the call's `options` last and joins the transaction of the
|
|
|
196
196
|
| Capability | Methods |
|
|
197
197
|
| --- | --- |
|
|
198
198
|
| `workspaces` | `findById(id, options)`, `findBySlug(slug, options)` (a deleted workspace is not found, though its slug stays taken), `listSlugsIn`, `findPersonalByOwnerUserId`, `listForUserId(userId, options)`, `insert`, `updateById`, `lock(workspaceId, options)` |
|
|
199
|
-
| `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds` |
|
|
199
|
+
| `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds`, `listActiveWorkspaceIdsByUserIdsAmong(userIds, workspaceIds, options)`, `joinSource({ alias, filters }, options)` |
|
|
200
200
|
|
|
201
201
|
`addMember` makes the user an active member with the role (`member` when none
|
|
202
202
|
is given), adding the membership or reactivating it. `activeMemberIds` answers
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
every active member's user id, without their profiles: it reads all of the
|
|
204
|
+
workspace's active memberships, so its cost grows with the workspace, and so
|
|
205
|
+
do `listActiveByWorkspaceId`'s and
|
|
206
|
+
`workspaces.services.members.listMembers(workspace, options)`'s. Use them where
|
|
207
|
+
the work needs every member, such as a job, a delivery's recipients or a
|
|
208
|
+
realtime audience. A screen or list reads a page instead:
|
|
209
|
+
`workspaces.services.members.listMembersPage(workspace, { cursor, limit }, options)`
|
|
210
|
+
answers one page of active members by display name (`limit`, 20 when none)
|
|
211
|
+
with the `nextCursor` that asks for the next, null on the last, and an app's
|
|
212
|
+
own list of members pages them in SQL with one report over the memberships'
|
|
213
|
+
`joinSource`. `lock` takes
|
|
205
214
|
the workspace's row lock until the caller's transaction ends, so work that must
|
|
206
215
|
not interleave for one workspace takes turns; it runs only inside a
|
|
207
216
|
transaction, given its `txOptions`, and answers `{ id }`, or `null` when there
|
|
@@ -336,6 +345,7 @@ authorization and role checks. Do not pass arbitrary HTTP input as options.
|
|
|
336
345
|
| `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` | Refuses to run inside a transaction: it delivers after the preparation's transaction commits. Reloads the committed pending invitation and workspace display data, validates expiry and token correspondence, and sends that invitation without replacement. |
|
|
337
346
|
| `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it. Refuses to run inside a transaction. |
|
|
338
347
|
| `workspaces.invite-acceptance-participant` (optional capability) | One application function receiving `({ invite, user }, options)` inside the acceptance transaction. Without it, acceptance activates the membership only. |
|
|
348
|
+
| `workspaces.workspace-created-participant` (optional capability) | One application function receiving `({ workspace, owner }, options)` inside a new workspace's creation transaction. Without it, creation writes the workspace, its owner's membership and its settings only. |
|
|
339
349
|
|
|
340
350
|
`sendInvite` accepts `{ createdInviteId, inviteTokenPreview }` from preparation or
|
|
341
351
|
only `{ createdInviteId }` for a later attempt. The latter uses JSKIT's existing
|
|
@@ -471,6 +481,24 @@ Replacement can create a new pending invitation after acceptance while retaining
|
|
|
471
481
|
the accepted invitation. Keep callbacks short: invitation changes in one workspace
|
|
472
482
|
serialize. Deadlocks/serialization failures propagate without replaying callbacks.
|
|
473
483
|
|
|
484
|
+
### Workspace creation participation
|
|
485
|
+
|
|
486
|
+
Provide the optional `workspaces.workspace-created-participant` capability the
|
|
487
|
+
same way, from a small application feature, to write application records for
|
|
488
|
+
every new workspace in the same transaction. Both creation paths call
|
|
489
|
+
`fn({ workspace, owner }, txOptions)`: the `workspace.workspaces.create` action
|
|
490
|
+
(`createWorkspaceForAuthenticatedUser`) and a personal workspace's provisioning
|
|
491
|
+
(`ensurePersonalWorkspaceForUser`, which joins its caller's transaction, such as
|
|
492
|
+
a first sign-in's profile sync). It runs after the workspace, its owner's
|
|
493
|
+
membership and its settings are written, so it can read them with `txOptions`.
|
|
494
|
+
`workspace` and `owner` are shallow frozen. Pass `txOptions` to every write and
|
|
495
|
+
let failures propagate: a failure rolls back the whole creation, workspace,
|
|
496
|
+
membership, settings and the participant's own writes, and the caller receives
|
|
497
|
+
the failure itself (an `AppError` answers as that refusal). Keep external effects
|
|
498
|
+
out of it. An existing personal workspace is not created again and does not call
|
|
499
|
+
it, and a create that loses its slug to the same owner's identical create answers
|
|
500
|
+
that workspace without calling it a second time.
|
|
501
|
+
|
|
474
502
|
### Compatibility and verification
|
|
475
503
|
|
|
476
504
|
Since `@jskit-ai/workspaces-core@0.1.198`, transactional `createInvite` callers
|
|
@@ -178,6 +178,10 @@ The check fails when:
|
|
|
178
178
|
- installed package versions or root/workspace declarations disagree with the lockfile.
|
|
179
179
|
- a package script runs tests outside the machine's test budget (`test/*`, see
|
|
180
180
|
[Test files run in parallel](/guide/framework/typed-features#test-files-run-in-parallel)).
|
|
181
|
+
- an application package's manifest (`jskit.capabilities`) misses a capability
|
|
182
|
+
its features provide or require, or provides one none of them does
|
|
183
|
+
(`manifest/capabilities`, see
|
|
184
|
+
[Package manifest](/guide/framework/typed-features#package-manifest)).
|
|
181
185
|
|
|
182
186
|
Shared client owners such as the kernel, HTTP runtime, shell, auth web runtime,
|
|
183
187
|
realtime runtime, and assistant runtime use exact peer dependencies where they
|
|
@@ -389,6 +389,29 @@ const emit = defineEmits(["close"]);
|
|
|
389
389
|
emit("closed");
|
|
390
390
|
```
|
|
391
391
|
|
|
392
|
+
### Selection fields
|
|
393
|
+
|
|
394
|
+
**A select, autocomplete or combobox whose options load from the server, or
|
|
395
|
+
whose value is a record id, is `SelectField` from
|
|
396
|
+
`@jskit-ai/http-web/client/components/SelectField`, with `loading` bound while
|
|
397
|
+
the options load.** A raw `v-select`, `v-autocomplete` or `v-combobox` takes
|
|
398
|
+
fixed options only: a list written out in the template, a top-level `const`
|
|
399
|
+
holding one (as written, frozen, or `computed()` over one written out), or an
|
|
400
|
+
import from a `shared` module. The model's name decides nothing.
|
|
401
|
+
|
|
402
|
+
Why: Vuetify titles a value missing from `items` with the value itself, so a
|
|
403
|
+
stored id such as `53` shows until the options arrive; `SelectField` hides it
|
|
404
|
+
while `loading` is true.
|
|
405
|
+
|
|
406
|
+
```vue
|
|
407
|
+
<!-- good -->
|
|
408
|
+
<SelectField v-model="form.serviceId" :items="services.items" :loading="services.loading" />
|
|
409
|
+
<v-select v-model="form.size" :items="SIZE_OPTIONS" />
|
|
410
|
+
|
|
411
|
+
<!-- bad: a stored id shows while the services load -->
|
|
412
|
+
<v-select v-model="form.serviceId" :items="services.items" />
|
|
413
|
+
```
|
|
414
|
+
|
|
392
415
|
### Presenting scripts
|
|
393
416
|
|
|
394
417
|
**A component's `<script>` presents: props, emits, element refs, display
|
|
@@ -496,6 +519,33 @@ let attachmentClientSequence = 0;
|
|
|
496
519
|
let pendingDraft = "";
|
|
497
520
|
```
|
|
498
521
|
|
|
522
|
+
### Search on the server
|
|
523
|
+
|
|
524
|
+
**A search over a list's records runs on the server: the list's search
|
|
525
|
+
(`useList({ search: { enabled: true } })`) sends what the person types as `q`,
|
|
526
|
+
and the table resource defines `searchSchema.q`. An autocomplete over a list's
|
|
527
|
+
items or a lookup's options takes `no-filter` and sends the typed text with
|
|
528
|
+
`@update:search` (a lookup's `setLookupSearch`).**
|
|
529
|
+
|
|
530
|
+
Why: a list holds the page it loaded, so a search over its items misses every
|
|
531
|
+
row past that page.
|
|
532
|
+
|
|
533
|
+
```vue
|
|
534
|
+
<!-- good: the server searches every record -->
|
|
535
|
+
<SelectField v-model="formState.serviceId" searchable no-filter
|
|
536
|
+
:search="resolveLookupSearch('serviceId')"
|
|
537
|
+
@update:search="setLookupSearch('serviceId', $event)"
|
|
538
|
+
:items="resolveLookupItems('serviceId', { selectedValue: formState.serviceId })" />
|
|
539
|
+
|
|
540
|
+
<!-- bad: Vuetify searches only the options already loaded -->
|
|
541
|
+
<v-autocomplete v-model="serviceId" :items="services.items" />
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
```js
|
|
545
|
+
// bad: a search over the page the list loaded
|
|
546
|
+
const visible = computed(() => list.items.filter((service) => service.name.includes(search.value)));
|
|
547
|
+
```
|
|
548
|
+
|
|
499
549
|
## Helpers and sizes
|
|
500
550
|
|
|
501
551
|
### One helper per package
|
|
@@ -563,6 +613,8 @@ client-only rules below. The server's action and error rules do not apply.
|
|
|
563
613
|
| `jskit/no-frozen-vue-state` | `Object.freeze` over refs, computeds or reactive state | [Fixed result shape](#fixed-result-shape) |
|
|
564
614
|
| `jskit/released-listeners` | a listener, interval, observer or subscription the file starts and never releases | [Released listeners](#released-listeners) |
|
|
565
615
|
| `jskit/no-module-state` | a module-level `let`, `ref()` or `reactive()` | [Module state](#module-state) |
|
|
616
|
+
| `jskit/selection-field` | a `v-select`, `v-autocomplete` or `v-combobox` whose items are not fixed, and a `SelectField` whose items are not fixed that binds no `loading` | [Selection fields](#selection-fields) |
|
|
617
|
+
| `jskit/search-on-server` | a `computed()` that filters a list composable's items by a search ref; an autocomplete over a list's items or a lookup's options without `no-filter` or an `update:search` handler | [Search on the server](#search-on-the-server) |
|
|
566
618
|
| `vue/require-prop-types`, `vue/require-default-prop`, `vue/require-valid-default-prop`, `vue/no-required-prop-with-default` | a prop without a type, optional without a default, or both | [Collaborator props](#collaborator-props) |
|
|
567
619
|
| `vue/require-explicit-emits` | an emitted event the component does not declare | [Declared emits](#declared-emits) |
|
|
568
620
|
|
|
@@ -583,10 +635,11 @@ Simplicity, effect order and presenting scripts are checked by reading.
|
|
|
583
635
|
Client exceptions use the
|
|
584
636
|
[typed features exceptions](/guide/framework/typed-features#exceptions): a
|
|
585
637
|
comment on the line before the finding, naming the rule or the finding, with a
|
|
586
|
-
reason prefix. Client code adds
|
|
638
|
+
reason prefix. Client code adds four placements:
|
|
587
639
|
|
|
588
640
|
| Placement | The exception |
|
|
589
641
|
| --- | --- |
|
|
590
642
|
| `// readable-check: allow module-state - shared resource: <what the page shares>` | module state every instance on the page shares on purpose |
|
|
591
643
|
| `<!-- readable-check: allow optional-call - optional operation: <what lacks it> -->` above an element | that element's bindings call an optional operation |
|
|
644
|
+
| `<!-- readable-check: allow selection-field - data: <where the options come from> -->` above an element | a selection whose options are in memory and whose value is no record id |
|
|
592
645
|
| `// readable-check: allow method-probe - input validation: <the shapes>` above a function | the probes of a function whose one job is normalizing an external value of several shapes |
|
|
@@ -256,6 +256,13 @@ mapping in the resource; do not add page-local filtering or duplicate request
|
|
|
256
256
|
code. The full Books source example maps `q` to title, author and notes; narrow
|
|
257
257
|
it when adapting the example to fewer fields.
|
|
258
258
|
|
|
259
|
+
Every CRUD list also takes `id`, one record id or a list of them
|
|
260
|
+
(`filter[id]` on the JSON:API transport), and answers only those of its rows,
|
|
261
|
+
in the caller's scope. A picker whose selected values are not on the page it
|
|
262
|
+
loaded reads them this way, all in one request, instead of showing their raw
|
|
263
|
+
ids or reading pages until it finds them; `useCrudListFilterLookups()` does it
|
|
264
|
+
for a lookup filter's chips and options.
|
|
265
|
+
|
|
259
266
|
The browser page's route query and internal CRUD query use plain `q`. The
|
|
260
267
|
JSON:API HTTP transport uses `filter[q]`. Shared CRUD clients encode and decode
|
|
261
268
|
this automatically. Raw HTTP/API tests must send the transport key themselves:
|
|
@@ -65,7 +65,9 @@ A grouped `aggregate` answers its groups from one statement, at most
|
|
|
65
65
|
`pageLimits().request` of them (the resource's `queryMaxLimit`). When there
|
|
66
66
|
are more it throws a 400 coded `RESOURCE_GROUP_LIMIT` instead of answering
|
|
67
67
|
part of them. Narrow the filters, or read the groups in pages, `page: { size,
|
|
68
|
-
number }` with `number` from 1
|
|
68
|
+
number }` with `number` from 1. A page answers as a `query` page does, `{ rows,
|
|
69
|
+
page: { size, number, hasMore, next } }`, never a bare array that could pass
|
|
70
|
+
for every group: read the next while `page.hasMore`.
|
|
69
71
|
Page numbers are offsets, so read every page before work that changes what
|
|
70
72
|
the filters match.
|
|
71
73
|
|
|
@@ -715,12 +717,12 @@ takes first.
|
|
|
715
717
|
// good: the lock is the transaction's first read, so the bookings read after it are current
|
|
716
718
|
const reserve = (seatId, options) => seats.withTransaction(options, async (txOptions) => {
|
|
717
719
|
await seats.lock(seatId, { ...txOptions, fields: ["status"] });
|
|
718
|
-
const taken = await bookings.queryAll({ filters: { seatIds: [seatId] } }, txOptions);
|
|
719
|
-
return taken
|
|
720
|
+
const [taken] = await bookings.queryAll({ filters: { seatIds: [seatId] }, take: 1 }, txOptions);
|
|
721
|
+
return taken ? null : bookings.create({ seatId }, { ...txOptions, returning: "id" });
|
|
720
722
|
});
|
|
721
723
|
|
|
722
724
|
// bad: the read before the lock fixed the snapshot, so a booking committed while the lock waited is missed
|
|
723
|
-
const
|
|
725
|
+
const [booked] = await bookings.queryAll({ filters: { seatIds: [seatId] }, take: 1 }, txOptions);
|
|
724
726
|
await seats.lock(seatId, txOptions);
|
|
725
727
|
```
|
|
726
728
|
|
|
@@ -931,7 +933,11 @@ async function activeDays(memberIds, options) {
|
|
|
931
933
|
`page.number` reads with OFFSET, which slows as the rows grow, so anything
|
|
932
934
|
that grows pages with `page.after`. Each page is one statement, with no
|
|
933
935
|
count: never walk pages to reach one.
|
|
934
|
-
- A screen list never uses `queryAll
|
|
936
|
+
- A screen list never uses `queryAll`, and neither does a picker: it reads
|
|
937
|
+
one page searched on the server as the person types (`q`), narrowed in the
|
|
938
|
+
query by what it depends on (the parent record, `active`), and reads its
|
|
939
|
+
selected values past that page by id. Reading every row for a dropdown, or
|
|
940
|
+
narrowing a page in code, either grows with the table or misses rows.
|
|
935
941
|
- Related rows come by id list (`filters: { id: ids }`, or an `in` filter on
|
|
936
942
|
another field) or `include`, grouped in memory. Never a query per row, however
|
|
937
943
|
the loop is spelled (`for`, `map` with `Promise.all`, a helper called per
|
|
@@ -987,6 +993,66 @@ const { records: firstPage } = await subscriptions.query({ filters: { newsletter
|
|
|
987
993
|
|
|
988
994
|
Bad: a list read 100-row pages in a loop for each of 40 related sets.
|
|
989
995
|
|
|
996
|
+
## Narrow in the query
|
|
997
|
+
|
|
998
|
+
A read asks the store for exactly the rows the code uses. The filter, sort,
|
|
999
|
+
limit, existence check and count go in the read, never in code over the rows
|
|
1000
|
+
it answered: a read of every row then narrowed is slow, and a page or a `take`
|
|
1001
|
+
then narrowed is also wrong, since the row wanted can sit past it.
|
|
1002
|
+
|
|
1003
|
+
- Filter with the resource's searchSchema: a field
|
|
1004
|
+
(`filters: { status: "open" }`), an id list (`filters: { id: ids }`, or an
|
|
1005
|
+
`in` filter on another field), or `q` for typed text, whose `oneOf` names the
|
|
1006
|
+
fields it searches.
|
|
1007
|
+
- One row: a unique filter with `take: 1` or `page: { size: 1 }`, which also
|
|
1008
|
+
answers whether one exists. A count: `aggregate()`.
|
|
1009
|
+
- Never filter, find, slice or count the rows a read answered, nor skip some
|
|
1010
|
+
in a loop. Mapping them and grouping them by a key (`Map.groupBy`) narrow
|
|
1011
|
+
nothing, and neither does code that needs every row it read: rows handed on
|
|
1012
|
+
whole (returned, passed, stored, mapped or walked in full) with a subset
|
|
1013
|
+
derived beside them, and rows matched against other items in a loop or a
|
|
1014
|
+
callback (an in-memory join, one read instead of one per item).
|
|
1015
|
+
- A lookup field's picker and a list's Search box send what the person types
|
|
1016
|
+
as `q` to the server, which searches every row; `useList` does with
|
|
1017
|
+
`search: { enabled: true }`. The table resource defines `searchSchema.q`.
|
|
1018
|
+
Never search the page a list loaded in the browser: it misses every row past
|
|
1019
|
+
it.
|
|
1020
|
+
- A CRUD list refuses a query key it does not take with a 400
|
|
1021
|
+
(`crud.query.undeclaredKeys`). A list filter is declared in the feature's
|
|
1022
|
+
`listFilterQueryValidator` and the resource's searchSchema.
|
|
1023
|
+
- Prove a capped read with a test that seeds past the cap and makes the row
|
|
1024
|
+
the cap leaves out (the oldest, for a newest-first read) the one that
|
|
1025
|
+
matches: the read that asks for it finds it; a read narrowed in code does
|
|
1026
|
+
not.
|
|
1027
|
+
|
|
1028
|
+
```js
|
|
1029
|
+
// good: the store answers one module's sections
|
|
1030
|
+
const { records } = await sections.query({ filters: { moduleId }, sort: ["-createdAt"], page: { size: 50 } }, options);
|
|
1031
|
+
|
|
1032
|
+
// bad: the newest 200 sections of every module, then one module's kept in code
|
|
1033
|
+
const { records: recent } = await sections.query({ sort: ["-createdAt"], page: { size: 200 } }, options);
|
|
1034
|
+
const moduleSections = recent.filter((section) => section.moduleId === moduleId);
|
|
1035
|
+
|
|
1036
|
+
// good: one row says who holds a unique username
|
|
1037
|
+
const [holder] = await profiles.queryAll({ filters: { usernames: [username] }, take: 1 }, options);
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
`jskit/narrow-in-query` reports `.filter()`, `.find()`, `.some()`,
|
|
1041
|
+
`.every()`, `.slice()`, a lone `.length` and a loop that skips rows with
|
|
1042
|
+
`continue` over the rows a read answered in the same function, unless the
|
|
1043
|
+
code also uses those rows whole, runs the narrowing once per item of
|
|
1044
|
+
something else, read at most one row, or slices a read that asked for one row
|
|
1045
|
+
more than it keeps (`limit(size + 1)`) to learn whether more follow.
|
|
1046
|
+
`jskit/search-on-server` reports a client search over a list's loaded items.
|
|
1047
|
+
`jskit check` fails a lookup field or a CRUD list screen whose app table
|
|
1048
|
+
resource has no `searchSchema.q`, on the resource or its owns entry
|
|
1049
|
+
(`data/search-q`). Other code that needs every row a read answers says why on
|
|
1050
|
+
the line above the read or the narrowing:
|
|
1051
|
+
|
|
1052
|
+
```js
|
|
1053
|
+
// readable-check: allow narrow-in-query - every row: a conversation holds at most keep + 1 summaries, since each save deletes the older ones
|
|
1054
|
+
```
|
|
1055
|
+
|
|
990
1056
|
## What changed since
|
|
991
1057
|
|
|
992
1058
|
A page that catches up on a chat, a watch that acts on changes and a sync to
|
|
@@ -1082,10 +1148,10 @@ const SettlementFeature = defineFeature({
|
|
|
1082
1148
|
async function settleDue(options) {
|
|
1083
1149
|
const size = cohorts.pageLimits().request;
|
|
1084
1150
|
const due = [];
|
|
1085
|
-
for (let number = 1; ;
|
|
1086
|
-
const page = await cohorts.aggregate({ measures: { cohorts: { count: "id" } }, groupBy: ["workspaceId"], filters: { status: "finished" }, page: { size, number } }, options);
|
|
1087
|
-
due.push(...
|
|
1088
|
-
|
|
1151
|
+
for (let number = 1; number; ) {
|
|
1152
|
+
const { rows, page } = await cohorts.aggregate({ measures: { cohorts: { count: "id" } }, groupBy: ["workspaceId"], filters: { status: "finished" }, page: { size, number } }, options);
|
|
1153
|
+
due.push(...rows);
|
|
1154
|
+
number = page.next;
|
|
1089
1155
|
}
|
|
1090
1156
|
for (const { workspaceId } of due) {
|
|
1091
1157
|
await settleWorkspace({ context: systemContext("settle finished weekly leagues for every user").forWorkspace(workspaceId) });
|
|
@@ -67,13 +67,13 @@ expects the relationship id or an unselected field never gets it.
|
|
|
67
67
|
### Every row
|
|
68
68
|
|
|
69
69
|
- A handle's `query()` answers one page and refuses a missing or oversized
|
|
70
|
-
`page.size`, where json-rest-api would cut the list silently. A
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
`page.size`, where json-rest-api would cut the list silently. A delivery
|
|
71
|
+
reaching every row uses `queryAll()`; a picker reads a page searched with
|
|
72
|
+
`q` and its selected values by `id`
|
|
73
73
|
([Reads](/guide/framework/data-access#reads-shaped-for-the-screen)).
|
|
74
74
|
- A grouped `aggregate()` answers every group or throws
|
|
75
75
|
`RESOURCE_GROUP_LIMIT`; past `pageLimits().request` groups, page it with
|
|
76
|
-
`page: { size, number }`
|
|
76
|
+
`page: { size, number }` (answers `{ rows, page }`)
|
|
77
77
|
([The handle's calls](/guide/framework/data-access#the-handle-s-calls)).
|
|
78
78
|
- A lookup by a unique key matches at most one row: `get(id)`, or a `query()`
|
|
79
79
|
of page size 1.
|
|
@@ -81,6 +81,31 @@ setup: ({ resources }) => ({ invoices: { list: (options) => resources.invoices.q
|
|
|
81
81
|
actions: ({ invoices }) => [{ id: "invoices.list", execute: () => invoices.list() }]
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Package manifest
|
|
85
|
+
|
|
86
|
+
**A package's manifest, `jskit.capabilities` in its `package.json`, lists
|
|
87
|
+
exactly what its features provide, and every capability they require beyond
|
|
88
|
+
the kernel's own (`runtime.actions`, `runtime.events`, `runtime.http`,
|
|
89
|
+
`runtime.bootstrap`) and those the package provides itself.** Optional
|
|
90
|
+
capabilities are not required. A requirement no feature has stays only when
|
|
91
|
+
the package needs it at install time, such as the database its migrations
|
|
92
|
+
need.
|
|
93
|
+
|
|
94
|
+
Why: start-up resolves every package's requirements from the manifests, and
|
|
95
|
+
refuses a manifest that disagrees with the package's features. Plain
|
|
96
|
+
`jskit check` loads each application package's features (the server providers
|
|
97
|
+
`jskit.runtime.server.providers` declares) and reports the drift before that
|
|
98
|
+
(`manifest/capabilities`), each capability to add or remove; it lists the
|
|
99
|
+
requirements no feature has for review.
|
|
100
|
+
|
|
101
|
+
```jsonc
|
|
102
|
+
// good: BooksFeature provides app.books and requires crud.contracts and runtime.actions
|
|
103
|
+
"capabilities": { "provides": ["app.books"], "requires": ["crud.contracts"] }
|
|
104
|
+
|
|
105
|
+
// bad: app.books missing from provides, crud.contracts from requires
|
|
106
|
+
"capabilities": { "provides": ["app.book"], "requires": [] }
|
|
107
|
+
```
|
|
108
|
+
|
|
84
109
|
### Capability types
|
|
85
110
|
|
|
86
111
|
**Each provided token has one `@typedef` in its package's `src/server/types.js`,
|
|
@@ -627,9 +652,9 @@ Why: a value with two defaults has two meanings, and a mode flag hides a
|
|
|
627
652
|
second method inside the first.
|
|
628
653
|
|
|
629
654
|
```js
|
|
630
|
-
// good
|
|
631
|
-
async function list(filters, options = {}) {
|
|
632
|
-
return invoices.
|
|
655
|
+
// good: one page, as a list shows it
|
|
656
|
+
async function list(filters, page, options = {}) {
|
|
657
|
+
return invoices.query({ filters, page }, options);
|
|
633
658
|
}
|
|
634
659
|
|
|
635
660
|
// bad
|
|
@@ -696,6 +721,11 @@ way (is this user an operator) is a member of its owner's capability.**
|
|
|
696
721
|
meaning of 0 and false before replacing a copy.
|
|
697
722
|
- A helper two packages of an application use lives in its shared package,
|
|
698
723
|
never inside one of them.
|
|
724
|
+
- A component's closure over its own state is not a helper: a function of at
|
|
725
|
+
most two statements in `<script setup>` whose every outside name is a
|
|
726
|
+
binding `<script setup>` declares (not an import or a global). Two pages
|
|
727
|
+
that reload their own resources the same way keep their own `reload()`;
|
|
728
|
+
sharing it would take a module that receives each page's state.
|
|
699
729
|
|
|
700
730
|
Why: the kernel's helpers fix one meaning of empty, and a copy brings its own.
|
|
701
731
|
|
|
@@ -746,6 +776,13 @@ handle's `queryAll(params, options)`. A `query()` names its page size, and
|
|
|
746
776
|
answers one page.** [Reads shaped for the screen](/guide/framework/data-access#reads-shaped-for-the-screen)
|
|
747
777
|
gives the rules and examples.
|
|
748
778
|
|
|
779
|
+
### Narrow in the query
|
|
780
|
+
|
|
781
|
+
**A read asks for exactly the rows the code uses: its filter, sort, limit,
|
|
782
|
+
existence check and count go in the read, never in code over the rows it
|
|
783
|
+
answered.** [Narrow in the query](/guide/framework/data-access#narrow-in-the-query)
|
|
784
|
+
gives the rules and examples.
|
|
785
|
+
|
|
749
786
|
### Lists read related rows in batches
|
|
750
787
|
|
|
751
788
|
**A read that builds a list fetches its related rows in batches, an id-list
|
|
@@ -1175,8 +1212,9 @@ Why: a comment that narrates history is wrong the day after it is written.
|
|
|
1175
1212
|
|
|
1176
1213
|
`npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
|
|
1177
1214
|
verify` runs `jskit check` (the package graph, navigation, [data
|
|
1178
|
-
access](/guide/framework/data-access#exceptions)
|
|
1179
|
-
runs](#test-run-rules)), lint, the
|
|
1215
|
+
access](/guide/framework/data-access#exceptions), [test
|
|
1216
|
+
runs](#test-run-rules) and [package manifests](#package-manifest)), lint, the
|
|
1217
|
+
tests and the build. The foundations'
|
|
1180
1218
|
`eslint.config.mjs` already holds a new
|
|
1181
1219
|
application's code to every check:
|
|
1182
1220
|
|
|
@@ -1246,6 +1284,7 @@ Every finding ends with a link to the section of this page that explains it.
|
|
|
1246
1284
|
| `jskit/app-error-code` | an `AppError`, `DomainError`, `ConflictError` or `NotFoundError` (or a subclass the module declares) without its own code, and one code naming two sentences in a module | [Refusals carry their own code](#refusals-carry-their-own-code) |
|
|
1247
1285
|
| `jskit/no-patched-error` | `Object.assign(new Error(…), { statusCode })`, or an error builder's result given its status afterwards | [One construction per error](#one-construction-per-error) |
|
|
1248
1286
|
| `jskit/no-test-only-export` | an export whose name says only tests use it (`…ForTests`, `__reset…`; `__testables` aside) | [No test-only exports](#no-test-only-exports) |
|
|
1287
|
+
| `jskit/narrow-in-query` | `.filter()`, `.find()`, `.some()`, `.every()`, `.slice()`, a lone `.length` or a loop that skips with `continue`, over the rows a read in the same function answered and uses no other way | [Narrow in the query](#narrow-in-the-query) |
|
|
1249
1288
|
| `jskit/one-property-per-line` | two properties on one line in a returned `Object.freeze({ … })` or a `requires` / `optional` / `provides` map (`eslint --fix` splits them) | [Formatter](#formatter) |
|
|
1250
1289
|
| `jskit/jsdoc-description-inside` | a `//` description directly above a `/** */` block | [Comments](#comments) |
|
|
1251
1290
|
| `jskit/allow-comment-form` | a `readable-check: allow` reason without a prefix from the table below, or one that cites tests or the state of a project | [Exceptions](#exceptions) |
|
|
@@ -1297,6 +1336,7 @@ if (typeof buildSerializer !== "function") {
|
|
|
1297
1336
|
| `filled by <repository>; convert with its supplier` | keeps a setter that another repository's code calls |
|
|
1298
1337
|
| `cross-repository slot <token>` | keeps a probe for a token another repository supplies |
|
|
1299
1338
|
| `shared resource:` | keeps client module state that every instance on the page shares on purpose ([client features](/guide/framework/client-features#module-state)) |
|
|
1339
|
+
| `every row: <why>` | code needs every row a read answers and narrows them for its own use, such as a read the data itself keeps small (a conversation's few summaries) whose oldest ones it deletes (`jskit/narrow-in-query`) |
|
|
1300
1340
|
| `infrastructure: <what the module is>` | excuses a whole file whose job is data access itself from `jskit/no-direct-knex` or `jskit/no-transaction-reader`, one line per rule before its first line of code ([data access](/guide/framework/data-access#exceptions)) |
|
|
1301
1341
|
|
|
1302
1342
|
One kind of probe gets one prefix and one wording everywhere, and a bare
|
package/package.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@jskit-ai/kernel": "0.2.
|
|
9
|
-
"json-rest-schema": "^1.0.
|
|
8
|
+
"@jskit-ai/kernel": "0.2.18",
|
|
9
|
+
"json-rest-schema": "^1.0.20"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
"./server/actions": "./src/server/bookingEngineActions.js",
|
|
@@ -74,7 +74,11 @@ Selection fields never show a stored id as text. A select, autocomplete or
|
|
|
74
74
|
combobox whose options load from the server, or whose value is a record id,
|
|
75
75
|
uses `SelectField` from `@jskit-ai/http-web/client/components/SelectField` with
|
|
76
76
|
`loading` while the options load, not a raw `v-select`, `v-autocomplete` or
|
|
77
|
-
`v-combobox`.
|
|
77
|
+
`v-combobox`. The lint rule `jskit/selection-field` reports a raw one whose
|
|
78
|
+
items are not fixed (a list written out, a top-level `const` holding one, or an
|
|
79
|
+
import from a `shared` module) and a `SelectField` over loaded items without
|
|
80
|
+
`loading`; a justified case says why in
|
|
81
|
+
`<!-- readable-check: allow selection-field - data: <reason> -->` above it.
|
|
78
82
|
|
|
79
83
|
Copied pattern source is ordinary application source. Do not add generator
|
|
80
84
|
provenance, receipts, completion ledgers, or tooling-operation history. Keep
|
|
@@ -34,23 +34,23 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
|
-
"@jskit-ai/kernel": "0.2.
|
|
37
|
+
"@jskit-ai/kernel": "0.2.18",
|
|
38
38
|
"@jskit-ai/material-web": "0.1.5",
|
|
39
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
40
40
|
"fastify": "^5.12.7",
|
|
41
41
|
"pino": "^10.3.1",
|
|
42
|
-
"json-rest-schema": "^1.0.
|
|
42
|
+
"json-rest-schema": "^1.0.20",
|
|
43
43
|
"pinia": "^4.0.3",
|
|
44
44
|
"vue": "^3.5.44",
|
|
45
45
|
"vue-router": "^5.3.1",
|
|
46
46
|
"vuetify": "^4.3.0",
|
|
47
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
47
|
+
"@jskit-ai/http-runtime": "0.2.18",
|
|
48
48
|
"@fastify/fast-json-stringify-compiler": "^5.1.0",
|
|
49
49
|
"@vue/devtools-api": "^8.2.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@jskit-ai/config-eslint": "0.1.
|
|
53
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
52
|
+
"@jskit-ai/config-eslint": "0.1.250",
|
|
53
|
+
"@jskit-ai/jskit-catalog": "0.2.46",
|
|
54
54
|
"@playwright/test": "1.61.1",
|
|
55
55
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
56
56
|
"eslint": "^10.8.0",
|
|
@@ -74,7 +74,11 @@ Selection fields never show a stored id as text. A select, autocomplete or
|
|
|
74
74
|
combobox whose options load from the server, or whose value is a record id,
|
|
75
75
|
uses `SelectField` from `@jskit-ai/http-web/client/components/SelectField` with
|
|
76
76
|
`loading` while the options load, not a raw `v-select`, `v-autocomplete` or
|
|
77
|
-
`v-combobox`.
|
|
77
|
+
`v-combobox`. The lint rule `jskit/selection-field` reports a raw one whose
|
|
78
|
+
items are not fixed (a list written out, a top-level `const` holding one, or an
|
|
79
|
+
import from a `shared` module) and a `SelectField` over loaded items without
|
|
80
|
+
`loading`; a justified case says why in
|
|
81
|
+
`<!-- readable-check: allow selection-field - data: <reason> -->` above it.
|
|
78
82
|
|
|
79
83
|
Copied pattern source is ordinary application source. Do not add generator
|
|
80
84
|
provenance, receipts, completion ledgers, or tooling-operation history. Keep
|
|
@@ -34,24 +34,24 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
|
-
"@jskit-ai/kernel": "0.2.
|
|
37
|
+
"@jskit-ai/kernel": "0.2.18",
|
|
38
38
|
"@jskit-ai/material-web": "0.1.5",
|
|
39
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
40
40
|
"fastify": "^5.12.7",
|
|
41
41
|
"pino": "^10.3.1",
|
|
42
|
-
"json-rest-schema": "^1.0.
|
|
42
|
+
"json-rest-schema": "^1.0.20",
|
|
43
43
|
"pinia": "^4.0.3",
|
|
44
44
|
"vue": "^3.5.44",
|
|
45
45
|
"vue-router": "^5.3.1",
|
|
46
46
|
"vuetify": "^4.3.0",
|
|
47
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
48
|
-
"@jskit-ai/shell-web": "0.2.
|
|
47
|
+
"@jskit-ai/http-runtime": "0.2.18",
|
|
48
|
+
"@jskit-ai/shell-web": "0.2.23",
|
|
49
49
|
"@fastify/fast-json-stringify-compiler": "^5.1.0",
|
|
50
50
|
"@vue/devtools-api": "^8.2.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@jskit-ai/config-eslint": "0.1.
|
|
54
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
53
|
+
"@jskit-ai/config-eslint": "0.1.250",
|
|
54
|
+
"@jskit-ai/jskit-catalog": "0.2.46",
|
|
55
55
|
"@playwright/test": "1.61.1",
|
|
56
56
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
57
57
|
"eslint": "^10.8.0",
|