@jskit-ai/agent-docs 0.1.291 → 0.1.293
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-setup/authentication.md +19 -8
- package/guide/agent/app-setup/multi-homing.md +19 -0
- package/guide/agent/app-setup/upgrading-jskit.md +4 -0
- package/guide/agent/framework/client-features.md +26 -1
- package/guide/agent/framework/data-access.md +118 -12
- package/guide/agent/framework/sanity-checks.md +4 -0
- package/guide/agent/framework/typed-features.md +33 -2
- 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/crud-core.md +1 -1
- package/reference/autogen/packages/kernel.md +7 -1
- package/reference/autogen/packages/resource-runtime.md +13 -4
- package/reference/autogen/packages/workspaces-core.md +8 -2
- package/reference/autogen/packages/workspaces-web.md +1 -0
- package/reference/autogen/tooling/config-eslint.md +18 -1
- package/reference/autogen/tooling/jskit-catalog.md +8 -0
- package/skills/jskit/references/client-features.md +26 -1
- package/skills/jskit/references/data-access.md +118 -12
- package/skills/jskit/references/heuristics/ui-testing.md +6 -3
- package/skills/jskit/references/multi-homing.md +19 -0
- 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 +3 -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/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/workspaces/workspace-server/PATTERN.md +9 -0
- package/skills/jskit/references/sanity-checks.md +4 -0
- package/skills/jskit/references/typed-features.md +33 -2
|
@@ -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
|
|
@@ -336,6 +336,7 @@ authorization and role checks. Do not pass arbitrary HTTP input as options.
|
|
|
336
336
|
| `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
337
|
| `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
338
|
| `workspaces.invite-acceptance-participant` (optional capability) | One application function receiving `({ invite, user }, options)` inside the acceptance transaction. Without it, acceptance activates the membership only. |
|
|
339
|
+
| `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
340
|
|
|
340
341
|
`sendInvite` accepts `{ createdInviteId, inviteTokenPreview }` from preparation or
|
|
341
342
|
only `{ createdInviteId }` for a later attempt. The latter uses JSKIT's existing
|
|
@@ -471,6 +472,24 @@ Replacement can create a new pending invitation after acceptance while retaining
|
|
|
471
472
|
the accepted invitation. Keep callbacks short: invitation changes in one workspace
|
|
472
473
|
serialize. Deadlocks/serialization failures propagate without replaying callbacks.
|
|
473
474
|
|
|
475
|
+
### Workspace creation participation
|
|
476
|
+
|
|
477
|
+
Provide the optional `workspaces.workspace-created-participant` capability the
|
|
478
|
+
same way, from a small application feature, to write application records for
|
|
479
|
+
every new workspace in the same transaction. Both creation paths call
|
|
480
|
+
`fn({ workspace, owner }, txOptions)`: the `workspace.workspaces.create` action
|
|
481
|
+
(`createWorkspaceForAuthenticatedUser`) and a personal workspace's provisioning
|
|
482
|
+
(`ensurePersonalWorkspaceForUser`, which joins its caller's transaction, such as
|
|
483
|
+
a first sign-in's profile sync). It runs after the workspace, its owner's
|
|
484
|
+
membership and its settings are written, so it can read them with `txOptions`.
|
|
485
|
+
`workspace` and `owner` are shallow frozen. Pass `txOptions` to every write and
|
|
486
|
+
let failures propagate: a failure rolls back the whole creation, workspace,
|
|
487
|
+
membership, settings and the participant's own writes, and the caller receives
|
|
488
|
+
the failure itself (an `AppError` answers as that refusal). Keep external effects
|
|
489
|
+
out of it. An existing personal workspace is not created again and does not call
|
|
490
|
+
it, and a create that loses its slug to the same owner's identical create answers
|
|
491
|
+
that workspace without calling it a second time.
|
|
492
|
+
|
|
474
493
|
### Compatibility and verification
|
|
475
494
|
|
|
476
495
|
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
|
|
@@ -563,6 +586,7 @@ client-only rules below. The server's action and error rules do not apply.
|
|
|
563
586
|
| `jskit/no-frozen-vue-state` | `Object.freeze` over refs, computeds or reactive state | [Fixed result shape](#fixed-result-shape) |
|
|
564
587
|
| `jskit/released-listeners` | a listener, interval, observer or subscription the file starts and never releases | [Released listeners](#released-listeners) |
|
|
565
588
|
| `jskit/no-module-state` | a module-level `let`, `ref()` or `reactive()` | [Module state](#module-state) |
|
|
589
|
+
| `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) |
|
|
566
590
|
| `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
591
|
| `vue/require-explicit-emits` | an emitted event the component does not declare | [Declared emits](#declared-emits) |
|
|
568
592
|
|
|
@@ -583,10 +607,11 @@ Simplicity, effect order and presenting scripts are checked by reading.
|
|
|
583
607
|
Client exceptions use the
|
|
584
608
|
[typed features exceptions](/guide/framework/typed-features#exceptions): a
|
|
585
609
|
comment on the line before the finding, naming the rule or the finding, with a
|
|
586
|
-
reason prefix. Client code adds
|
|
610
|
+
reason prefix. Client code adds four placements:
|
|
587
611
|
|
|
588
612
|
| Placement | The exception |
|
|
589
613
|
| --- | --- |
|
|
590
614
|
| `// readable-check: allow module-state - shared resource: <what the page shares>` | module state every instance on the page shares on purpose |
|
|
591
615
|
| `<!-- readable-check: allow optional-call - optional operation: <what lacks it> -->` above an element | that element's bindings call an optional operation |
|
|
616
|
+
| `<!-- 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
617
|
| `// 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 |
|
|
@@ -34,7 +34,7 @@ through its handle's calls, and every call takes the call's `options` last:
|
|
|
34
34
|
| Call | Does |
|
|
35
35
|
| --- | --- |
|
|
36
36
|
| `query({ filters, sort, page, include }, options)` | one page: `{ records, page }` |
|
|
37
|
-
| `queryAll({ filters, sort, include }, options)` | every matching row, for jobs, id lists and one parent's rows |
|
|
37
|
+
| `queryAll({ filters, sort, include, take }, options)` | every matching row, or the first `take`, for jobs, id lists and one parent's rows |
|
|
38
38
|
| `queryPages({ filters, sort, include }, options)` | every matching row, a page at a time, for work over many rows |
|
|
39
39
|
| `get(id, params, options)` | one record |
|
|
40
40
|
| `aggregate({ measures, groupBy, filters, sort, page }, options)` | totals, grouped by fields or date buckets |
|
|
@@ -261,6 +261,63 @@ the other table hides, which would tell a caller which rows hold a value it
|
|
|
261
261
|
may not read. For either, the other table's owner answers the ids it matches
|
|
262
262
|
through its owner API, and the filter takes the list: `filters: { supplierIds }`.
|
|
263
263
|
|
|
264
|
+
A filter that asks whether a row has, or lacks, another owner's rows
|
|
265
|
+
(projects with an open task, projects with no task) reads that owner's join
|
|
266
|
+
source. The `owns` entry's `searchSchema` names each join source by an alias,
|
|
267
|
+
with a function answering its owner from the feature's `requires` or its own
|
|
268
|
+
handles (`owned`), and `applyFilter` gets `joinSource(alias)`: that owner's
|
|
269
|
+
`{ query, column }` for the caller's scope, as a report's join source
|
|
270
|
+
([Layer 5](#layer-5-reports-the-resource-cannot-express)), which it ties to
|
|
271
|
+
the row with `whereColumn` inside `whereExists` or `whereNotExists`:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
// the projects feature's owns entry; the feature requires the tasks owner as `tasks`
|
|
275
|
+
{
|
|
276
|
+
resource: projectResource,
|
|
277
|
+
searchSchema: {
|
|
278
|
+
hasOpenTask: {
|
|
279
|
+
type: "boolean",
|
|
280
|
+
joinSources: { k: ({ requires }) => requires.tasks },
|
|
281
|
+
applyFilter(query, wanted, { column, joinSource }) {
|
|
282
|
+
const task = joinSource("k");
|
|
283
|
+
const openTasks = task.query
|
|
284
|
+
.select(task.column("id"))
|
|
285
|
+
.whereColumn(task.column("projectId"), column("id"))
|
|
286
|
+
.where(task.column("status"), "open");
|
|
287
|
+
if (wanted) {
|
|
288
|
+
query.whereExists(openTasks);
|
|
289
|
+
} else {
|
|
290
|
+
query.whereNotExists(openTasks);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The handle builds the join sources a read's filters use just before the read,
|
|
299
|
+
in the call's own context and transaction, so `query`, `queryAll`,
|
|
300
|
+
`queryPages`, `aggregate` and `joinSource` take the filter in their one
|
|
301
|
+
statement, and the other owner's ownership, row policy and hidden fields
|
|
302
|
+
decide which of its rows count (`column()` refuses a hidden field). A filter
|
|
303
|
+
that prefers one match and falls back on another when no row of the table has
|
|
304
|
+
the first (the projects named like the term, or, when none is, those with a
|
|
305
|
+
task in that status) names the feature's own handle for its own table,
|
|
306
|
+
`({ owned }) => owned.projects`. Only a read through a handle builds join
|
|
307
|
+
sources. `own()` refuses, at startup, `joinSources` that is not a record of
|
|
308
|
+
functions under aliases, such a filter with `oneOf` or without `applyFilter`,
|
|
309
|
+
and `joinSources` in the table resource's own `searchSchema`, which names no
|
|
310
|
+
feature to answer it.
|
|
311
|
+
|
|
312
|
+
An app switches to such a filter where a search resolves another owner's
|
|
313
|
+
matches first: a repository that reads the other owner's ids (an id report on
|
|
314
|
+
its `scopedQuery()`, or that owner's `queryAll` matched in memory) and then
|
|
315
|
+
filters `id` by the list, or a filter that was a `whereRaw` with `EXISTS
|
|
316
|
+
(SELECT ... FROM ...)`, which `jskit/sql-literal` refuses. The filter moves
|
|
317
|
+
into the `owns` entry with the other owner as a join source, the code that
|
|
318
|
+
resolved ids goes, and the read is one statement however many rows match,
|
|
319
|
+
where the id list grew with the matches.
|
|
320
|
+
|
|
264
321
|
A table keyed by a `uuid` or `string` primary key takes its ids as text; an
|
|
265
322
|
`owns` entry's `normalizeId` is only for an id written some other way.
|
|
266
323
|
|
|
@@ -569,8 +626,9 @@ waiter with errno 1020 ("Record has changed since last read"), which a write
|
|
|
569
626
|
that owns its transaction runs again. For one row,
|
|
570
627
|
`patch(id, changes, { ...options, onlyIf: { status: "open" } })` does the same.
|
|
571
628
|
A filter on another table (a search field whose `actualField` reaches a
|
|
572
|
-
related table) is refused on MySQL and
|
|
573
|
-
`selection_cross_table`), even when it
|
|
629
|
+
related table, or one that reads a join source) is refused on MySQL and
|
|
630
|
+
MariaDB (`RestApiValidationError`, rule `selection_cross_table`), even when it
|
|
631
|
+
matches no row; `lockWhere` and `claim` refuse it the same way. A search entry's own
|
|
574
632
|
`applyFilter` that reads another table through a subquery is not detected, and
|
|
575
633
|
is no compare-and-set there either. Filter on the row's own columns instead, for example
|
|
576
634
|
on the link after locking its targets with `lockMany` in the same transaction
|
|
@@ -627,17 +685,31 @@ or a counter row taken with `lock(id)`. Every lock needs a transaction; SQLite
|
|
|
627
685
|
runs one transaction at a time, so there is no row lock to take. Take locks in
|
|
628
686
|
one order on every path, a parent before its children.
|
|
629
687
|
|
|
630
|
-
A lock answers
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
688
|
+
A lock answers the rows it locks, with the fields `fields` names. Its
|
|
689
|
+
`include` names belongs-to relationships (`include: ["customer"]`, or
|
|
690
|
+
`"batch.yard"` a level further): after its own rows, the lock locks the
|
|
691
|
+
records they name through each related table's own handle (its scope, its row
|
|
692
|
+
policy, its hidden fields), in id order, each related table once however many
|
|
693
|
+
rows name it, and nests them as a read's include does; a link to a record the
|
|
694
|
+
caller may not see reads as in a read, its belongs-to field null. Those are locks too, so on MySQL and MariaDB
|
|
695
|
+
they find a record committed after the transaction's first plain read, which a
|
|
696
|
+
plain read misses. They come after the rows, a child before the parent it
|
|
697
|
+
names, so a path that locks the parent first and then its children (to add one)
|
|
698
|
+
takes the other order: the database ends a deadlock between the two by failing
|
|
699
|
+
one transaction, which `withTransaction` runs again, and a path that runs
|
|
700
|
+
often keeps one order on both sides. A collection (`include: ["lines"]`) is
|
|
701
|
+
refused before any statement: lock a parent's children with their own
|
|
702
|
+
table's `lockWhere` in the same transaction.
|
|
703
|
+
|
|
704
|
+
Every other read after the lock is a plain read. On PostgreSQL every statement
|
|
705
|
+
sees what had committed when it began, so that read is current. On MySQL and
|
|
706
|
+
MariaDB a plain read inside a transaction sees the snapshot of the
|
|
636
707
|
transaction's first plain read, and a lock that waited does not move it. So
|
|
637
708
|
take the lock before any plain read in the transaction: the reads after it then
|
|
638
709
|
see what the lock's previous holder committed. A related row the work decides
|
|
639
|
-
on (is this seat already booked?) is locked itself,
|
|
640
|
-
that every writer of it
|
|
710
|
+
on (is this seat already booked?) is locked itself, through the lock's
|
|
711
|
+
`include` or a lock of its own, or read after a lock that every writer of it
|
|
712
|
+
takes first.
|
|
641
713
|
|
|
642
714
|
```js
|
|
643
715
|
// good: the lock is the transaction's first read, so the bookings read after it are current
|
|
@@ -802,7 +874,10 @@ async function activeDays(memberIds, options) {
|
|
|
802
874
|
its own row with `whereColumn`; one that counts them joins the source grouped,
|
|
803
875
|
as `projectSummary` does. Rows matched by a related row's value ("items
|
|
804
876
|
whose supplier is named Acme") need no report: a cross-table search filter
|
|
805
|
-
answers them in a plain read ([Layer 1](#layer-1-the-table-resource)).
|
|
877
|
+
answers them in a plain read ([Layer 1](#layer-1-the-table-resource)). Nor
|
|
878
|
+
does a question that only narrows a plain read's rows (has an open task, has
|
|
879
|
+
no task): a search filter that reads the other owner's join source answers
|
|
880
|
+
it in that read, with its page, sort and includes as usual (Layer 1).
|
|
806
881
|
|
|
807
882
|
```js
|
|
808
883
|
// Knex: report - the caller's projects with no task the caller sees
|
|
@@ -816,6 +891,32 @@ async function activeDays(memberIds, options) {
|
|
|
816
891
|
}
|
|
817
892
|
```
|
|
818
893
|
|
|
894
|
+
- A report in a feature that owns none of its tables (a console's, over
|
|
895
|
+
several owners) roots at the main owner's `joinSource()` instead of a
|
|
896
|
+
`scopedQuery()`, and ties the other owners' join sources to it the same way,
|
|
897
|
+
in one statement, every owner's scope and row policy applied.
|
|
898
|
+
|
|
899
|
+
```js
|
|
900
|
+
// Knex: report - the caller's projects with no task the caller sees, for a console that owns neither table
|
|
901
|
+
async function idleProjects(options) {
|
|
902
|
+
const project = await projects.joinSource({ alias: "p" }, options);
|
|
903
|
+
const task = await tasks.joinSource({ alias: "k" }, options);
|
|
904
|
+
return project.query
|
|
905
|
+
.whereNotExists(task.query.select(task.column("id")).whereColumn(task.column("projectId"), project.column("id")))
|
|
906
|
+
.select({ name: project.column("name") });
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
- An app switches a report it kept as a raw-SQL exception (another owner's
|
|
911
|
+
table joined by name, or a subquery in `raw()`, in a file under an
|
|
912
|
+
infrastructure exception, see [Exceptions](#exceptions)) to these: the
|
|
913
|
+
report's feature requires each owner whose rows it reads, puts each owner's
|
|
914
|
+
join source where the table stood (`whereExists`, `whereNotExists`, or
|
|
915
|
+
joined grouped for counts and sums), roots at its own table's
|
|
916
|
+
`scopedQuery()` or at the main owner's `joinSource()`, and the file drops
|
|
917
|
+
its `readable-check: allow` lines and its `jskit-data-access.json` entry.
|
|
918
|
+
When an owner is a JSKIT package, its owner API answers `joinSource`
|
|
919
|
+
([Tables you don't own](#tables-you-dont-own)).
|
|
819
920
|
- When each owner can answer on its own (keys in, grouped totals out), the
|
|
820
921
|
report is a feature of its own that calls each owner's method and merges the
|
|
821
922
|
answers in memory: one statement per owner, never a call per row.
|
|
@@ -843,6 +944,11 @@ async function activeDays(memberIds, options) {
|
|
|
843
944
|
request it stops past the request row limit (`pageLimits().requestRows`,
|
|
844
945
|
10,000 rows) with a 400 coded `RESOURCE_ROW_LIMIT`, and never answers part
|
|
845
946
|
of the rows. A job reads without that limit and holds every row in memory.
|
|
947
|
+
`queryAll({ ..., take: 500 })` answers only the first 500 rows in its sort
|
|
948
|
+
order and reads no row past them, the last page no larger than the rows still
|
|
949
|
+
wanted. `take` is then the read's bound, so a request may name more than the
|
|
950
|
+
request row limit and never gets `RESOURCE_ROW_LIMIT`: name the bound the
|
|
951
|
+
work can hold, never one from the request unchecked.
|
|
846
952
|
- Work over many rows reads `queryPages` and handles one page at a time:
|
|
847
953
|
`for await (const records of handle.queryPages(params, options))`. Each
|
|
848
954
|
page is one statement of up to `pageLimits().server` rows (fewer when its
|
|
@@ -116,6 +116,10 @@ expects the relationship id or an unselected field never gets it.
|
|
|
116
116
|
once](/guide/framework/typed-features#test-databases-are-prepared-once),
|
|
117
117
|
[integration test
|
|
118
118
|
databases](/guide/app-setup/database-layer#integration-test-databases)).
|
|
119
|
+
- A component's content and states are checked in a Vitest component test;
|
|
120
|
+
a browser check is an assertion in the Playwright spec that already reaches
|
|
121
|
+
that screen. Every Playwright run ends with its 10 slowest tests: compare a
|
|
122
|
+
new test's time, and the suite's, before and after.
|
|
119
123
|
|
|
120
124
|
### Inputs and access
|
|
121
125
|
|
|
@@ -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`,
|
|
@@ -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
|
|
|
@@ -1175,8 +1205,9 @@ Why: a comment that narrates history is wrong the day after it is written.
|
|
|
1175
1205
|
|
|
1176
1206
|
`npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
|
|
1177
1207
|
verify` runs `jskit check` (the package graph, navigation, [data
|
|
1178
|
-
access](/guide/framework/data-access#exceptions)
|
|
1179
|
-
runs](#test-run-rules)), lint, the
|
|
1208
|
+
access](/guide/framework/data-access#exceptions), [test
|
|
1209
|
+
runs](#test-run-rules) and [package manifests](#package-manifest)), lint, the
|
|
1210
|
+
tests and the build. The foundations'
|
|
1180
1211
|
`eslint.config.mjs` already holds a new
|
|
1181
1212
|
application's code to every check:
|
|
1182
1213
|
|
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.249",
|
|
53
|
+
"@jskit-ai/jskit-catalog": "0.2.45",
|
|
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.22",
|
|
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.249",
|
|
54
|
+
"@jskit-ai/jskit-catalog": "0.2.45",
|
|
55
55
|
"@playwright/test": "1.61.1",
|
|
56
56
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
57
57
|
"eslint": "^10.8.0",
|