@jskit-ai/agent-docs 0.1.275 → 0.1.277
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/database-layer.md +30 -0
- package/guide/agent/app-setup/material-3-migration.md +32 -2
- package/guide/agent/framework/material-3.md +9 -2
- package/guide/agent/framework/typed-features.md +207 -0
- package/package.json +1 -1
- package/patterns/minimal-foundation/example/package.json +2 -2
- package/patterns/shell-foundation/example/package.json +3 -3
- package/reference/autogen/PATTERN_INDEX.md +15 -15
- package/reference/autogen/packages/material-web.md +1 -1
- package/reference/autogen/packages/shell-web.md +30 -1
- package/reference/autogen/tooling/jskit-catalog.md +7 -1
- package/skills/jskit/SKILL.md +6 -0
- package/skills/jskit/references/heuristics/INDEX.md +2 -2
- package/skills/jskit/references/heuristics/row-policies.md +10 -0
- package/skills/jskit/references/heuristics/typed-features.md +12 -0
- package/skills/jskit/references/heuristics/ui-testing.md +9 -5
- package/skills/jskit/references/material-3.md +9 -2
- package/skills/jskit/references/pattern-index.md +15 -15
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +2 -2
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +3 -3
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/typed-features.md +207 -0
|
@@ -365,6 +365,36 @@ Do not add migration receipts, sync ledgers, generator provenance, or dialect
|
|
|
365
365
|
questionnaires. The installed graph, migration source, environment, and
|
|
366
366
|
database migration table are sufficient.
|
|
367
367
|
|
|
368
|
+
## Integration test databases
|
|
369
|
+
|
|
370
|
+
Disposable integration and migration tests use `TEST_DB_NAME`. A test run
|
|
371
|
+
prepares that schema once and restores it from a stored snapshot; it never
|
|
372
|
+
rebuilds a database by replaying the whole migration history:
|
|
373
|
+
|
|
374
|
+
- The snapshot holds the schema as the engine reports it (each table's
|
|
375
|
+
`SHOW CREATE TABLE` on MySQL and MariaDB, `pg_dump --schema-only` on
|
|
376
|
+
PostgreSQL), the migration ledger rows and the seed rows. Its key is a digest
|
|
377
|
+
of the migration files that the application's `migrations/` and its installed
|
|
378
|
+
packages supply, and of the seed source.
|
|
379
|
+
- When that digest changes, the harness migrates one database once, outside
|
|
380
|
+
the timed tests, and stores a new snapshot. Otherwise it restores the stored
|
|
381
|
+
one. On PostgreSQL, `CREATE DATABASE <name> TEMPLATE <prepared>` copies a
|
|
382
|
+
prepared database that has no open connections.
|
|
383
|
+
- A test that runs migrations up or down, or otherwise changes the schema,
|
|
384
|
+
leaves its database dirty. The harness restores the snapshot before another
|
|
385
|
+
test uses that database, instead of running `migrate.latest()` over the
|
|
386
|
+
whole history.
|
|
387
|
+
- Only the fresh-schema check in [Verification](#verification) replays the
|
|
388
|
+
whole history, as its own step.
|
|
389
|
+
- The harness reports each phase (digest, restore or rebuild, seed) with its
|
|
390
|
+
duration, and the suite has a time budget, so slow preparation is visible
|
|
391
|
+
instead of hiding inside the next test that waits for it. A slow phase is
|
|
392
|
+
fixed, never absorbed by a longer time limit.
|
|
393
|
+
|
|
394
|
+
Endpoints and batch operations get their own database tests at realistic row
|
|
395
|
+
counts with a fixed query budget; see the typed features guide's
|
|
396
|
+
[budgets at realistic counts](/guide/framework/typed-features#budgets-at-realistic-counts).
|
|
397
|
+
|
|
368
398
|
## Browser test preparation
|
|
369
399
|
|
|
370
400
|
Keep the test schema and migration history between routine browser runs. The
|
|
@@ -52,8 +52,9 @@ export const theme = {
|
|
|
52
52
|
- A colour the app really owns (a partner brand, a status of its own) goes in
|
|
53
53
|
`colors` by camelCase name, such as `partner: "#0A7D3E"`. It becomes
|
|
54
54
|
`partner`, `on-partner`, `partner-container` and `on-partner-container`,
|
|
55
|
-
harmonized to the seed, in both schemes
|
|
56
|
-
|
|
55
|
+
harmonized to the seed, in both schemes; `{ value: "#F5C518", blend: false }`
|
|
56
|
+
keeps a brand colour's own hue. `success`, `warning` and `info` are already
|
|
57
|
+
there.
|
|
57
58
|
- Delete `defaultTheme: "light"`. The app now follows the device's light or
|
|
58
59
|
dark setting.
|
|
59
60
|
|
|
@@ -114,6 +115,9 @@ arrow. The shell's Up arrow returns to the page the person came from, or else
|
|
|
114
115
|
to the parent, so:
|
|
115
116
|
|
|
116
117
|
- delete "Back to X" buttons, `router.back()` and `history.back()` calls;
|
|
118
|
+
- rename a back button for a step or panel inside a page to what it does
|
|
119
|
+
("Previous step") or where it goes ("Dashboard"), without the back arrow.
|
|
120
|
+
An embedded frame's own Back and Forward, such as a preview browser's, stay;
|
|
117
121
|
- delete redirects from a settings or section landing page to its first child.
|
|
118
122
|
A settings page is a list-detail page (`ShellListDetail`): on wide windows the
|
|
119
123
|
list stays beside the open section, and on narrow ones the list is the
|
|
@@ -135,6 +139,32 @@ the content into the app bar:
|
|
|
135
139
|
At most one action is `primary`; on compact windows the others show as icon
|
|
136
140
|
buttons named by their label.
|
|
137
141
|
|
|
142
|
+
A layout that draws its own top app bar instead of `ShellLayout` keeps it and
|
|
143
|
+
builds the page's part from the shell's pieces, so Up, the title and the
|
|
144
|
+
actions behave as in `ShellLayout`:
|
|
145
|
+
|
|
146
|
+
```vue
|
|
147
|
+
<script setup>
|
|
148
|
+
import { ShellPageActionsTarget, ShellPageTitle, ShellUpButton, useShellAppBar } from "@jskit-ai/shell-web/client";
|
|
149
|
+
|
|
150
|
+
useShellAppBar();
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<v-app-bar>
|
|
155
|
+
<ShellUpButton />
|
|
156
|
+
<ShellPageTitle title="Workspace" />
|
|
157
|
+
<ShellPageActionsTarget />
|
|
158
|
+
<!-- the app's own controls -->
|
|
159
|
+
</v-app-bar>
|
|
160
|
+
<v-main><slot /></v-main>
|
|
161
|
+
</template>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Delete the bar's own back button and the pages' own `h1`s: `ShellPageTitle` is
|
|
165
|
+
the page's one `h1`, and `jskit check` now counts the layout as a shell layout,
|
|
166
|
+
so its inner pages need parents.
|
|
167
|
+
|
|
138
168
|
## 7. CRUD screens
|
|
139
169
|
|
|
140
170
|
The CRUD screens apply all of this themselves. An app that customized them
|
|
@@ -112,7 +112,7 @@ Exactly this, and nothing more:
|
|
|
112
112
|
| `nav/history` | `history.back()`, `history.go()`, `history.pushState()`/`replaceState()`, `router.back()`, `router.go()` |
|
|
113
113
|
| `nav/window-location` | `window.location` assignments, `.href =`, `location.assign()`, `location.replace()` |
|
|
114
114
|
| `nav/raw-link` | a hand-built in-app path in `to`, `href`, `router.push()`, `router.replace()` or a `to:`/`href:` object key outside a placement |
|
|
115
|
-
| `nav/back-control` | any back control but the shell's Up: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component, a `goBack()`-style handler |
|
|
115
|
+
| `nav/back-control` | any back control but the shell's Up navigating the app: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component, a `goBack()`-style handler |
|
|
116
116
|
| `nav/missing-parent` | an inner page of a shell surface without a declared parent |
|
|
117
117
|
| `nav/top-level-parent` | a main-navigation destination that declares a parent |
|
|
118
118
|
| `nav/allow` | any `m3-check: allow nav/...` comment: nothing excuses a navigation rule |
|
|
@@ -145,6 +145,9 @@ Exactly this, and nothing more:
|
|
|
145
145
|
`finish(recordUrl)`; the record shows, and Up goes where the record's Up went
|
|
146
146
|
before. Wrong: `router.push(recordUrl)` after Save, so Up or Back reopens the
|
|
147
147
|
form.
|
|
148
|
+
- **Steps inside a page are named actions:** "Previous step" or the
|
|
149
|
+
destination's name, not "Back" or a back arrow. A frame's own Back
|
|
150
|
+
(`contentWindow.history`) is fine.
|
|
148
151
|
|
|
149
152
|
## Foundation
|
|
150
153
|
|
|
@@ -196,6 +199,9 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
|
|
|
196
199
|
standalone heading. A section inside a page uses a title role.
|
|
197
200
|
- **Up** sits in the bar on every inner page (see Navigation and Up). On phones
|
|
198
201
|
it replaces the menu icon.
|
|
202
|
+
- **A layout with its own top app bar** calls `useShellAppBar()` and renders
|
|
203
|
+
`ShellUpButton`, `ShellPageTitle` and `ShellPageActionsTarget` in it;
|
|
204
|
+
`jskit check` treats it as a shell layout.
|
|
199
205
|
- **Actions:** a screen's actions go in the bar inside `ShellPageActions`
|
|
200
206
|
(`@jskit-ai/shell-web/client/components/ShellPageActions`), each a
|
|
201
207
|
`ShellPageAction` with `icon`, `label` (also its accessible name and compact
|
|
@@ -322,7 +328,8 @@ Take the approved route first; none needs an exception:
|
|
|
322
328
|
|
|
323
329
|
- **Brand or partner colours** are theme custom colours, used by name:
|
|
324
330
|
`colors: { partner: "#0A7D3E" }` in `src/theme/index.js`, then
|
|
325
|
-
`<v-chip color="partner">`.
|
|
331
|
+
`<v-chip color="partner">`. `{ value: "#F5C518", blend: false }` keeps
|
|
332
|
+
its hue.
|
|
326
333
|
- **Chart and data colours** come from the theme: `const theme = useTheme()`,
|
|
327
334
|
then `["primary", "tertiary"].map((role) => theme.current.value.colors[role])`.
|
|
328
335
|
- **A third-party widget** lives in one wrapper component, such as
|
|
@@ -724,6 +724,107 @@ const rows = extractJsonRestCollectionRows(await api.resources.workspaceMembersh
|
|
|
724
724
|
}, createJsonRestContext(options.context)));
|
|
725
725
|
```
|
|
726
726
|
|
|
727
|
+
### One read per table
|
|
728
|
+
|
|
729
|
+
**A read that builds a list reads each table once for the whole list: one
|
|
730
|
+
query per table with an id-list filter, the rows grouped in memory. It never
|
|
731
|
+
reads one row, or runs one lookup, per item of the list.**
|
|
732
|
+
|
|
733
|
+
- Collect the ids from the first read, then read each related table once with
|
|
734
|
+
an id-list filter: a `searchSchema` entry
|
|
735
|
+
`{ type: "array", actualField: "<column>", filterOperator: "in" }`, read
|
|
736
|
+
through `queryAllJsonRestCollectionRows()`, or the resource's `include`.
|
|
737
|
+
Group the rows by id with `Map.groupBy()`.
|
|
738
|
+
- `getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
|
|
739
|
+
single-record read inside a loop over the list is a read per row, however
|
|
740
|
+
the loop is spelled (`for`, `map` with `Promise.all`, a helper called per
|
|
741
|
+
item). So is a single-record helper reused for a list.
|
|
742
|
+
- Work derived from a row (hashing, preparing a form, assembling a snapshot)
|
|
743
|
+
runs once per distinct input in the call, not once per row it appears in.
|
|
744
|
+
- The list's query count follows its tables and pages (100 rows a page unless
|
|
745
|
+
the resource sets `queryMaxLimit`), never its rows: three hundred invoices
|
|
746
|
+
and their lines take a handful of queries, not three hundred more (see
|
|
747
|
+
[Budgets at realistic counts](#budgets-at-realistic-counts)).
|
|
748
|
+
|
|
749
|
+
Why: a read per row costs nothing with three test rows and seconds with a few
|
|
750
|
+
hundred real ones, and every lookup and visibility check inside it repeats too.
|
|
751
|
+
|
|
752
|
+
```js
|
|
753
|
+
// good: one read per table, however many invoices there are
|
|
754
|
+
const invoiceRows = await queryAllJsonRestCollectionRows(api.resources.invoices, {
|
|
755
|
+
queryParams: { filters: { workspace: workspaceId } },
|
|
756
|
+
transaction: resolveTransaction(options),
|
|
757
|
+
format: "plain"
|
|
758
|
+
}, options.context);
|
|
759
|
+
const lineRows = await queryAllJsonRestCollectionRows(api.resources.invoiceLines, {
|
|
760
|
+
queryParams: { filters: { invoiceIds: invoiceRows.map((invoice) => invoice.id) } },
|
|
761
|
+
transaction: resolveTransaction(options),
|
|
762
|
+
format: "plain"
|
|
763
|
+
}, options.context);
|
|
764
|
+
const linesByInvoice = Map.groupBy(lineRows, (line) => line.invoiceId);
|
|
765
|
+
return invoiceRows.map((invoice) => ({ ...invoice, lines: linesByInvoice.get(invoice.id) || [] }));
|
|
766
|
+
|
|
767
|
+
// bad: one more query, and its lookups and row policy, for every invoice
|
|
768
|
+
for (const invoice of invoiceRows) {
|
|
769
|
+
invoice.lines = await invoiceLines.queryDocuments({ invoiceId: invoice.id }, options);
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Bounded access checks
|
|
774
|
+
|
|
775
|
+
**A row policy costs what the rows its query reads cost, never what the
|
|
776
|
+
subject's stored history costs, and a write decides access once instead of
|
|
777
|
+
paying a read policy for every row it writes.**
|
|
778
|
+
|
|
779
|
+
- A `rowPolicy` adds one predicate to the query it is given: a join, `EXISTS`
|
|
780
|
+
or subquery correlated with the row being read, on indexed columns. It never
|
|
781
|
+
loads everything the subject was ever linked to (every source, notice or
|
|
782
|
+
grant in their history) to build an `IN` list, and never calls another
|
|
783
|
+
owner's access check once per item of that history. The
|
|
784
|
+
[row policies heuristic](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/heuristics/row-policies.md)
|
|
785
|
+
has the policy contract and its package composition.
|
|
786
|
+
- The policy runs on every single-record read, and a CRUD write includes one:
|
|
787
|
+
`createDocument()` and `patchDocumentById()` write with `returning: "full"`
|
|
788
|
+
and read the record back through the policy, and `patch` and `delete` first
|
|
789
|
+
look the record up through it. A write therefore pays the read policy for
|
|
790
|
+
each row it writes.
|
|
791
|
+
- A write path decides access once, inside its transaction. A batch write (a
|
|
792
|
+
delivery, an import, a fan-out) whose per-row read-back would pay an
|
|
793
|
+
expensive policy writes through a repository method added with
|
|
794
|
+
`decorateRepository`, on `resolveRepoClient(database.knex, options)`,
|
|
795
|
+
setting the same columns the resource would, its ownership columns
|
|
796
|
+
included, and reading back only what it returns.
|
|
797
|
+
- Its test runs one read and one write for a subject with a short history and
|
|
798
|
+
with a realistic long one, and asserts the same query count.
|
|
799
|
+
|
|
800
|
+
Why: a check sized by history grows with every record the subject receives,
|
|
801
|
+
so the same write that takes milliseconds in a test takes seconds a year later.
|
|
802
|
+
|
|
803
|
+
```js
|
|
804
|
+
// good: one predicate on the row being read
|
|
805
|
+
function noticeVisibilityPolicy({ query, context, column, value, db }) {
|
|
806
|
+
const userId = resolveActorId(context);
|
|
807
|
+
if (!userId) return false;
|
|
808
|
+
query.where(column("recipientId"), value("recipientId", userId)).whereExists(
|
|
809
|
+
db("sources").select(1)
|
|
810
|
+
.whereColumn("sources.id", column("sourceId"))
|
|
811
|
+
.where("sources.status", "published")
|
|
812
|
+
);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// bad: every source the recipient was ever notified about, checked one by one
|
|
817
|
+
async function noticeVisibilityPolicy({ query, context, column, value }) {
|
|
818
|
+
const sourceIds = await noticeHistory.listSourceIds(resolveActorId(context));
|
|
819
|
+
const readable = [];
|
|
820
|
+
for (const sourceId of sourceIds) {
|
|
821
|
+
if (await sources.canRead(sourceId, context)) readable.push(sourceId);
|
|
822
|
+
}
|
|
823
|
+
query.whereIn(column("sourceId"), readable.map((id) => value("sourceId", id)));
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
727
828
|
### Routes
|
|
728
829
|
|
|
729
830
|
**A route does HTTP work only (status, cookies, redirects) and runs an action
|
|
@@ -910,6 +1011,112 @@ export function resetInvoiceCacheForTests() {
|
|
|
910
1011
|
}
|
|
911
1012
|
```
|
|
912
1013
|
|
|
1014
|
+
### Budgets at realistic counts
|
|
1015
|
+
|
|
1016
|
+
**A list, batch or write path has a database test at the row counts and
|
|
1017
|
+
history a real workspace holds, and that test fails above a fixed query
|
|
1018
|
+
budget, one that a read per row or a history-sized check cannot meet.**
|
|
1019
|
+
|
|
1020
|
+
- Seed hundreds of records and long per-person histories, not one to three
|
|
1021
|
+
rows: a read per row and a history-sized policy cost nothing at three.
|
|
1022
|
+
- Count the queries with the test Knex instance's `query` event and assert a
|
|
1023
|
+
ceiling: the count the correct implementation needs at that size, which a
|
|
1024
|
+
read per row overshoots by hundreds. A path that pages nothing (one read,
|
|
1025
|
+
one write) asserts the same count at a short and a long history. A query
|
|
1026
|
+
count is stable in CI; also report the time the call took.
|
|
1027
|
+
- Meet the budget by fixing the reads ([One read per
|
|
1028
|
+
table](#one-read-per-table), [Bounded access checks](#bounded-access-checks)),
|
|
1029
|
+
never by shrinking the data or raising the budget.
|
|
1030
|
+
|
|
1031
|
+
Why: without a budget at real counts, a path that does one read per row ships,
|
|
1032
|
+
and its first sign is a request that takes seconds in production.
|
|
1033
|
+
|
|
1034
|
+
```js
|
|
1035
|
+
// good
|
|
1036
|
+
test("the invoice list reads each table once", async () => {
|
|
1037
|
+
await seedInvoices(knex, { workspace, count: 300, linesEach: 5 });
|
|
1038
|
+
let queries = 0;
|
|
1039
|
+
const count = () => { queries += 1; };
|
|
1040
|
+
knex.on("query", count);
|
|
1041
|
+
try {
|
|
1042
|
+
await invoices.listWithLines(workspace, { context });
|
|
1043
|
+
} finally {
|
|
1044
|
+
knex.off("query", count);
|
|
1045
|
+
}
|
|
1046
|
+
// A page of each table at a time; one read per invoice would add 300.
|
|
1047
|
+
assert.ok(queries <= 40, `${queries} queries`);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// bad: three invoices and no budget, so a read per invoice passes
|
|
1051
|
+
test("the invoice list has its lines", async () => {
|
|
1052
|
+
await seedInvoices(knex, { workspace, count: 3, linesEach: 1 });
|
|
1053
|
+
assert.equal((await invoices.listWithLines(workspace, { context })).length, 3);
|
|
1054
|
+
});
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### Time limits are not fixes
|
|
1058
|
+
|
|
1059
|
+
**A slow test, suite, preparation or request is a defect to find, never a time
|
|
1060
|
+
limit to raise. Measure where the time goes, fix that cause, and keep the
|
|
1061
|
+
default limits.**
|
|
1062
|
+
|
|
1063
|
+
- Raising `--test-timeout`, a test's `{ timeout }`, Playwright's `timeout`,
|
|
1064
|
+
`test.setTimeout()` or an `expect` timeout, adding retries, or calling a
|
|
1065
|
+
request in a loop until it answers hides the cause and lets it grow.
|
|
1066
|
+
- Read the preparation phases' durations and the query counts first; the
|
|
1067
|
+
cause is usually a read per row, a history-sized policy, or a database
|
|
1068
|
+
prepared by replaying migrations ([Test databases are prepared
|
|
1069
|
+
once](#test-databases-are-prepared-once)).
|
|
1070
|
+
- A longer limit is right only for work that is long by nature, such as a
|
|
1071
|
+
fixture of deliberate size, and the test names that work.
|
|
1072
|
+
- A time limit still bounds every run, so a hang ends; it is not a speed
|
|
1073
|
+
setting.
|
|
1074
|
+
|
|
1075
|
+
Why: a raised limit absorbs today's slowness and every later one, until a run
|
|
1076
|
+
takes hours and nobody knows which change made it slow.
|
|
1077
|
+
|
|
1078
|
+
```js
|
|
1079
|
+
// good: the slow step was found and fixed, so the default limit holds
|
|
1080
|
+
test("delivers a batch of notices", async () => {
|
|
1081
|
+
await deliveries.processBatch(workspace, { context });
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// bad
|
|
1085
|
+
test("delivers a batch of notices", { timeout: 600_000 }, async () => {
|
|
1086
|
+
await deliveries.processBatch(workspace, { context });
|
|
1087
|
+
});
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Test databases are prepared once
|
|
1091
|
+
|
|
1092
|
+
**A test run prepares each database once, from a stored snapshot of its
|
|
1093
|
+
schema, migration ledger and seed rows checked by a digest of their sources,
|
|
1094
|
+
and never replays the whole migration history inside the run.**
|
|
1095
|
+
|
|
1096
|
+
- The snapshot is rebuilt only when the digest of the migration and seed
|
|
1097
|
+
sources changes, once and outside the timed tests; every disposable
|
|
1098
|
+
database, and every database a test migrated up or down, is restored from
|
|
1099
|
+
it.
|
|
1100
|
+
- The full migration history runs once, in its own fresh-schema check
|
|
1101
|
+
([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
|
|
1102
|
+
- The harness reports each preparation phase and its duration, and the suite
|
|
1103
|
+
has a time budget, so slow preparation shows instead of hiding inside the
|
|
1104
|
+
next test that waits for it.
|
|
1105
|
+
- The database layer guide's [integration test
|
|
1106
|
+
databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)
|
|
1107
|
+
gives the snapshot's contents per engine.
|
|
1108
|
+
|
|
1109
|
+
Why: the migration history only grows, so a run that replays it whenever a
|
|
1110
|
+
schema changed gets slower with every migration the application adds.
|
|
1111
|
+
|
|
1112
|
+
```js
|
|
1113
|
+
// good: the application's test support restores the snapshot its digest names
|
|
1114
|
+
before(async () => restoreTestDatabase(knex, await testDatabaseSnapshot()));
|
|
1115
|
+
|
|
1116
|
+
// bad: every file replays every migration
|
|
1117
|
+
before(() => knex.migrate.latest());
|
|
1118
|
+
```
|
|
1119
|
+
|
|
913
1120
|
## Files and layout
|
|
914
1121
|
|
|
915
1122
|
### Size limits
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
37
|
"@jskit-ai/kernel": "0.2.10",
|
|
38
|
-
"@jskit-ai/material-web": "0.1.
|
|
38
|
+
"@jskit-ai/material-web": "0.1.1",
|
|
39
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
40
40
|
"fastify": "^5.12.7",
|
|
41
41
|
"pino": "^10.3.1",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jskit-ai/config-eslint": "0.1.245",
|
|
53
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
53
|
+
"@jskit-ai/jskit-catalog": "0.2.29",
|
|
54
54
|
"@playwright/test": "1.61.1",
|
|
55
55
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
56
56
|
"eslint": "^10.8.0",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
37
|
"@jskit-ai/kernel": "0.2.10",
|
|
38
|
-
"@jskit-ai/material-web": "0.1.
|
|
38
|
+
"@jskit-ai/material-web": "0.1.1",
|
|
39
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
40
40
|
"fastify": "^5.12.7",
|
|
41
41
|
"pino": "^10.3.1",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"vue-router": "^5.3.1",
|
|
46
46
|
"vuetify": "^4.3.0",
|
|
47
47
|
"@jskit-ai/http-runtime": "0.2.10",
|
|
48
|
-
"@jskit-ai/shell-web": "0.2.
|
|
48
|
+
"@jskit-ai/shell-web": "0.2.12",
|
|
49
49
|
"@fastify/fast-json-stringify-compiler": "^5.1.0",
|
|
50
50
|
"@vue/devtools-api": "^8.2.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jskit-ai/config-eslint": "0.1.245",
|
|
54
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
54
|
+
"@jskit-ai/jskit-catalog": "0.2.29",
|
|
55
55
|
"@playwright/test": "1.61.1",
|
|
56
56
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
57
57
|
"eslint": "^10.8.0",
|
|
@@ -15,7 +15,7 @@ A concrete Fastify, Vue, and JSKIT application foundation for products that do n
|
|
|
15
15
|
|
|
16
16
|
- Id: `app/minimal-foundation`
|
|
17
17
|
- Keywords: `app`, `fastify`, `foundation`, `minimal`, `server`, `vite`, `vue`
|
|
18
|
-
- Owner: `@jskit-ai/agent-docs@0.1.
|
|
18
|
+
- Owner: `@jskit-ai/agent-docs@0.1.277`
|
|
19
19
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md)
|
|
20
20
|
- Examples: [example/](../../skills/jskit/references/patterns/app/minimal-foundation/example/)
|
|
21
21
|
- Requires: `@jskit-ai/http-runtime`, `@jskit-ai/kernel`
|
|
@@ -26,7 +26,7 @@ A concrete JSKIT web application foundation with responsive shell navigation, se
|
|
|
26
26
|
|
|
27
27
|
- Id: `app/shell-foundation`
|
|
28
28
|
- Keywords: `app`, `foundation`, `material`, `navigation`, `placements`, `shell`, `vite`, `vue`
|
|
29
|
-
- Owner: `@jskit-ai/agent-docs@0.1.
|
|
29
|
+
- Owner: `@jskit-ai/agent-docs@0.1.277`
|
|
30
30
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/app/shell-foundation/PATTERN.md)
|
|
31
31
|
- Examples: [example/](../../skills/jskit/references/patterns/app/shell-foundation/example/)
|
|
32
32
|
- Requires: `@jskit-ai/http-runtime`, `@jskit-ai/kernel`, `@jskit-ai/shell-web`
|
|
@@ -37,7 +37,7 @@ Configure an assistant runtime for one application surface and expose its chat a
|
|
|
37
37
|
|
|
38
38
|
- Id: `assistant/assistant-surface`
|
|
39
39
|
- Keywords: `ai`, `assistant`, `chat`, `config`, `environment`, `page`, `placement`, `settings`, `surface`
|
|
40
|
-
- Owner: `@jskit-ai/assistant-runtime@0.2.
|
|
40
|
+
- Owner: `@jskit-ai/assistant-runtime@0.2.25`
|
|
41
41
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md)
|
|
42
42
|
- Examples: [example/](../../skills/jskit/references/patterns/assistant/assistant-surface/example/)
|
|
43
43
|
- Requires: `@jskit-ai/assistant-runtime`, `@jskit-ai/connectors-catalog`, `@jskit-ai/connectors-core`, `@jskit-ai/shell-web`
|
|
@@ -59,7 +59,7 @@ Compose JSKIT authentication routes, views, profile controls, and public surface
|
|
|
59
59
|
|
|
60
60
|
- Id: `auth/auth-surface`
|
|
61
61
|
- Keywords: `account`, `auth`, `login`, `logout`, `password`, `placement`, `profile`, `reset`, `surface`
|
|
62
|
-
- Owner: `@jskit-ai/auth-web@0.2.
|
|
62
|
+
- Owner: `@jskit-ai/auth-web@0.2.14`
|
|
63
63
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/auth/auth-surface/PATTERN.md)
|
|
64
64
|
- Examples: [example/](../../skills/jskit/references/patterns/auth/auth-surface/example/)
|
|
65
65
|
- Requires: `@jskit-ai/auth-core`, `@jskit-ai/auth-web`, `@jskit-ai/shell-web`
|
|
@@ -70,7 +70,7 @@ Let people sign in with the accounts they already have in an accounts hub or ano
|
|
|
70
70
|
|
|
71
71
|
- Id: `auth/oidc-sign-in`
|
|
72
72
|
- Keywords: `accounts hub`, `auth`, `authentication`, `back-channel logout`, `dynamic client registration`, `issuer`, `master login`, `oidc`, `openid connect`, `single sign-on`, `sso`
|
|
73
|
-
- Owner: `@jskit-ai/auth-provider-oidc-core@0.2.
|
|
73
|
+
- Owner: `@jskit-ai/auth-provider-oidc-core@0.2.16`
|
|
74
74
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/auth/oidc-sign-in/PATTERN.md)
|
|
75
75
|
- Examples: [example/](../../skills/jskit/references/patterns/auth/oidc-sign-in/example/)
|
|
76
76
|
- Requires: `@jskit-ai/auth-provider-oidc-core`, `@jskit-ai/auth-web`
|
|
@@ -246,7 +246,7 @@ Add a protected administration surface, settings shell, and semantic navigation
|
|
|
246
246
|
|
|
247
247
|
- Id: `console/console-surface`
|
|
248
248
|
- Keywords: `admin`, `console`, `navigation`, `owner`, `placement`, `settings`, `surface`
|
|
249
|
-
- Owner: `@jskit-ai/console-web@0.1.
|
|
249
|
+
- Owner: `@jskit-ai/console-web@0.1.236`
|
|
250
250
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/console/console-surface/PATTERN.md)
|
|
251
251
|
- Examples: [example/](../../skills/jskit/references/patterns/console/console-surface/example/)
|
|
252
252
|
- Requires: `@jskit-ai/console-core`, `@jskit-ai/console-web`, `@jskit-ai/shell-web`
|
|
@@ -257,7 +257,7 @@ Build a routed CRUD user interface as thin application pages over JSKIT's shared
|
|
|
257
257
|
|
|
258
258
|
- Id: `crud/crud-screen-set`
|
|
259
259
|
- Keywords: `actions`, `add`, `crud`, `delete`, `edit`, `filters`, `list`, `material`, `routes`, `view`, `vue`
|
|
260
|
-
- Owner: `@jskit-ai/http-web@0.2.
|
|
260
|
+
- Owner: `@jskit-ai/http-web@0.2.17`
|
|
261
261
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md)
|
|
262
262
|
- Examples: [example/](../../skills/jskit/references/patterns/crud/crud-screen-set/example/)
|
|
263
263
|
- Requires: `@jskit-ai/http-web`, `@jskit-ai/resource-crud-core`
|
|
@@ -312,7 +312,7 @@ Wrap a JSKIT web application in a Capacitor Android shell using native Capacitor
|
|
|
312
312
|
|
|
313
313
|
- Id: `mobile/android-application`
|
|
314
314
|
- Keywords: `android`, `capacitor`, `device`, `mobile`, `native`, `shell`, `webview`
|
|
315
|
-
- Owner: `@jskit-ai/mobile-capacitor@0.2.
|
|
315
|
+
- Owner: `@jskit-ai/mobile-capacitor@0.2.12`
|
|
316
316
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/mobile/android-application/PATTERN.md)
|
|
317
317
|
- Examples: [example/](../../skills/jskit/references/patterns/mobile/android-application/example/)
|
|
318
318
|
- Requires: `@capacitor/android`, `@capacitor/app`, `@capacitor/cli`, `@jskit-ai/mobile-capacitor`
|
|
@@ -323,7 +323,7 @@ Add JSKIT realtime events with an optional Redis backplane and an explicit shell
|
|
|
323
323
|
|
|
324
324
|
- Id: `realtime/realtime-application`
|
|
325
325
|
- Keywords: `realtime`, `redis`, `socket.io`, `sockets`, `status`, `websocket`
|
|
326
|
-
- Owner: `@jskit-ai/realtime@0.2.
|
|
326
|
+
- Owner: `@jskit-ai/realtime@0.2.15`
|
|
327
327
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md)
|
|
328
328
|
- Examples: [example/](../../skills/jskit/references/patterns/realtime/realtime-application/example/)
|
|
329
329
|
- Requires: `@jskit-ai/realtime`, `@jskit-ai/shell-web`
|
|
@@ -334,7 +334,7 @@ Compose application-owned Google Publisher Tag delivery with the generic rewarde
|
|
|
334
334
|
|
|
335
335
|
- Id: `rewards/google-rewarded`
|
|
336
336
|
- Keywords: `ads`, `google`, `publisher tag`, `rewarded`, `unlock`
|
|
337
|
-
- Owner: `@jskit-ai/rewarded-web@0.2.
|
|
337
|
+
- Owner: `@jskit-ai/rewarded-web@0.2.14`
|
|
338
338
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md)
|
|
339
339
|
- Examples: [example/](../../skills/jskit/references/patterns/rewards/google-rewarded/example/)
|
|
340
340
|
- Requires: `@jskit-ai/rewarded-web`
|
|
@@ -345,7 +345,7 @@ Define a server feature through explicit capabilities and first-class actions, a
|
|
|
345
345
|
|
|
346
346
|
- Id: `server/feature-package`
|
|
347
347
|
- Keywords: `actions`, `feature`, `json-rest`, `knex`, `orchestration`, `package`, `provider`, `repository`, `routes`, `server`
|
|
348
|
-
- Owner: `@jskit-ai/agent-docs@0.1.
|
|
348
|
+
- Owner: `@jskit-ai/agent-docs@0.1.277`
|
|
349
349
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/server/feature-package/PATTERN.md)
|
|
350
350
|
- Examples: [example/](../../skills/jskit/references/patterns/server/feature-package/example/)
|
|
351
351
|
- Requires: `@jskit-ai/kernel`
|
|
@@ -356,7 +356,7 @@ Compose the JSKIT responsive shell, semantic placements, settings navigation, an
|
|
|
356
356
|
|
|
357
357
|
- Id: `shell/application-shell`
|
|
358
358
|
- Keywords: `adaptive`, `app`, `layout`, `navigation`, `placement`, `responsive`, `settings`, `shell`
|
|
359
|
-
- Owner: `@jskit-ai/shell-web@0.2.
|
|
359
|
+
- Owner: `@jskit-ai/shell-web@0.2.12`
|
|
360
360
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/shell/application-shell/PATTERN.md)
|
|
361
361
|
- Examples: [example/](../../skills/jskit/references/patterns/shell/application-shell/example/)
|
|
362
362
|
- Requires: `@jskit-ai/kernel`, `@jskit-ai/shell-web`
|
|
@@ -367,7 +367,7 @@ Add product routes and shell extensions through file routing, semantic placement
|
|
|
367
367
|
|
|
368
368
|
- Id: `ui/page-and-placement`
|
|
369
369
|
- Keywords: `component`, `navigation`, `outlet`, `page`, `placement`, `routes`, `section`, `shell`, `subpages`, `vue`
|
|
370
|
-
- Owner: `@jskit-ai/shell-web@0.2.
|
|
370
|
+
- Owner: `@jskit-ai/shell-web@0.2.12`
|
|
371
371
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/ui/page-and-placement/PATTERN.md)
|
|
372
372
|
- Examples: [example/](../../skills/jskit/references/patterns/ui/page-and-placement/example/)
|
|
373
373
|
- Requires: `@jskit-ai/kernel`, `@jskit-ai/shell-web`
|
|
@@ -378,7 +378,7 @@ Compose an account settings route and profile, preference, notification, and sig
|
|
|
378
378
|
|
|
379
379
|
- Id: `users/account-settings`
|
|
380
380
|
- Keywords: `account`, `link provider`, `notifications`, `oauth`, `preferences`, `profile`, `settings`, `sign-in methods`, `user`, `vue`
|
|
381
|
-
- Owner: `@jskit-ai/users-web@0.2.
|
|
381
|
+
- Owner: `@jskit-ai/users-web@0.2.19`
|
|
382
382
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/users/account-settings/PATTERN.md)
|
|
383
383
|
- Examples: [example/](../../skills/jskit/references/patterns/users/account-settings/example/)
|
|
384
384
|
- Requires: `@jskit-ai/shell-web`, `@jskit-ai/users-core`, `@jskit-ai/users-web`
|
|
@@ -411,7 +411,7 @@ Compose workspace selection, invitation, member administration, settings, and re
|
|
|
411
411
|
|
|
412
412
|
- Id: `workspaces/workspace-surfaces`
|
|
413
413
|
- Keywords: `admin`, `invite`, `member`, `navigation`, `settings`, `surface`, `switcher`, `workspace`
|
|
414
|
-
- Owner: `@jskit-ai/workspaces-web@0.2.
|
|
414
|
+
- Owner: `@jskit-ai/workspaces-web@0.2.19`
|
|
415
415
|
- Read: [PATTERN.md](../../skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md)
|
|
416
416
|
- Examples: [example/](../../skills/jskit/references/patterns/workspaces/workspace-surfaces/example/)
|
|
417
417
|
- Requires: `@jskit-ai/shell-web`, `@jskit-ai/workspaces-core`, `@jskit-ai/workspaces-web`
|
|
@@ -21,7 +21,7 @@ Local functions
|
|
|
21
21
|
- `kebabCase(name)`
|
|
22
22
|
- `channels(argb)`
|
|
23
23
|
- `customColorRoleNames(name)`
|
|
24
|
-
- `addCustomColorRoles(roles, seedArgb, dark, name, argb)`
|
|
24
|
+
- `addCustomColorRoles(roles, seedArgb, dark, name, argb, blend = true)`
|
|
25
25
|
- `schemeColors(scheme, seedArgb, dark, appColors)`
|
|
26
26
|
- `vuetifyTheme(seedArgb, dark, appColors)`
|
|
27
27
|
- `normalizeAppColors(colors, schemeRoles)`
|
|
@@ -82,7 +82,6 @@ Exports
|
|
|
82
82
|
- None
|
|
83
83
|
Local functions
|
|
84
84
|
- `updateScrolled()`
|
|
85
|
-
- `goUp()`
|
|
86
85
|
|
|
87
86
|
### `src/client/components/ShellListDetail.vue`
|
|
88
87
|
Exports
|
|
@@ -123,6 +122,14 @@ Local functions
|
|
|
123
122
|
Exports
|
|
124
123
|
- None
|
|
125
124
|
|
|
125
|
+
### `src/client/components/ShellPageActionsTarget.vue`
|
|
126
|
+
Exports
|
|
127
|
+
- None
|
|
128
|
+
|
|
129
|
+
### `src/client/components/ShellPageTitle.vue`
|
|
130
|
+
Exports
|
|
131
|
+
- None
|
|
132
|
+
|
|
126
133
|
### `src/client/components/ShellRouteTransition.vue`
|
|
127
134
|
Exports
|
|
128
135
|
- None
|
|
@@ -135,12 +142,22 @@ Exports
|
|
|
135
142
|
Exports
|
|
136
143
|
- None
|
|
137
144
|
|
|
145
|
+
### `src/client/components/ShellUpButton.vue`
|
|
146
|
+
Exports
|
|
147
|
+
- None
|
|
148
|
+
Local functions
|
|
149
|
+
- `goUp()`
|
|
150
|
+
|
|
138
151
|
### `src/client/composables/shellLayoutDrawerPreference.js`
|
|
139
152
|
Exports
|
|
140
153
|
- `SHELL_LAYOUT_DRAWER_DEFAULT_OPEN_STORAGE_KEY`
|
|
141
154
|
- `readDrawerDefaultOpenPreference({ storage = pageStorage() } = {})`
|
|
142
155
|
- `writeDrawerDefaultOpenPreference(open, { storage = pageStorage() } = {})`
|
|
143
156
|
|
|
157
|
+
### `src/client/composables/useShellAppBar.js`
|
|
158
|
+
Exports
|
|
159
|
+
- `useShellAppBar()`
|
|
160
|
+
|
|
144
161
|
### `src/client/composables/useShellDrawer.js`
|
|
145
162
|
Exports
|
|
146
163
|
- `useShellDrawer({ props, modalLayout, viewportWidth, navigationToggle, layoutState })`
|
|
@@ -278,6 +295,10 @@ Exports
|
|
|
278
295
|
- `ShellPageActions`
|
|
279
296
|
- `ShellPageAction`
|
|
280
297
|
- `ShellListDetail`
|
|
298
|
+
- `ShellUpButton`
|
|
299
|
+
- `ShellPageTitle`
|
|
300
|
+
- `ShellPageActionsTarget`
|
|
301
|
+
- `useShellAppBar`
|
|
281
302
|
- `useShellLayoutState`
|
|
282
303
|
- `useShellPageTitle`
|
|
283
304
|
- `useShellWindowSizeClass`
|
|
@@ -640,6 +661,10 @@ Local functions
|
|
|
640
661
|
- `createSurfacePlacements(surface)`
|
|
641
662
|
- `refuseBlockedRecord(to)`
|
|
642
663
|
|
|
664
|
+
### `fixtures/adaptive-shell/src/OwnBarLayout.vue`
|
|
665
|
+
Exports
|
|
666
|
+
- None
|
|
667
|
+
|
|
643
668
|
### `fixtures/adaptive-shell/src/RecordListPage.vue`
|
|
644
669
|
Exports
|
|
645
670
|
- None
|
|
@@ -660,6 +685,10 @@ Exports
|
|
|
660
685
|
Exports
|
|
661
686
|
- None
|
|
662
687
|
|
|
688
|
+
### `fixtures/adaptive-shell/src/StudioPage.vue`
|
|
689
|
+
Exports
|
|
690
|
+
- None
|
|
691
|
+
|
|
663
692
|
### `fixtures/adaptive-shell/src/theme/index.js`
|
|
664
693
|
Exports
|
|
665
694
|
- `theme`
|
|
@@ -124,7 +124,7 @@ Local functions
|
|
|
124
124
|
### `scripts/navigation.mjs`
|
|
125
125
|
Exports
|
|
126
126
|
- `NAVIGATION_RULES`
|
|
127
|
-
- `checkBackControl(tag, report)`
|
|
127
|
+
- `checkBackControl(tag, report, script = "")`
|
|
128
128
|
- `checkLinkAttribute(tag, entry, report)`
|
|
129
129
|
- `checkNavigationCode(code, offset, report)`
|
|
130
130
|
- `checkPageParents({ root, appFiles, pageFiles, report })`
|
|
@@ -134,6 +134,10 @@ Exports
|
|
|
134
134
|
Local functions
|
|
135
135
|
- `compact(text)`
|
|
136
136
|
- `isPlacementLink(code, offset)`
|
|
137
|
+
- `definitionOf(script, name)`
|
|
138
|
+
- `statementFrom(text, start)`
|
|
139
|
+
- `navigatesApp(code, script, seen = new Set())`
|
|
140
|
+
- `navigatesFromControl(tag, script)`
|
|
137
141
|
- `attribute(tag, name)`
|
|
138
142
|
- `visibleText(inner)`
|
|
139
143
|
- `isBackTag(tag)`
|
|
@@ -149,6 +153,8 @@ Local functions
|
|
|
149
153
|
- `surfaceOf(route, surfaces)`
|
|
150
154
|
- `routeDeclarations(source)`
|
|
151
155
|
- `readPage(file, source)`
|
|
156
|
+
- `createShellBarReader(appFiles, read)`
|
|
157
|
+
- `localComponent(file, specifier, known)`
|
|
152
158
|
- `pageKey(file)`
|
|
153
159
|
|
|
154
160
|
### `scripts/pattern-assets.mjs`
|
package/skills/jskit/SKILL.md
CHANGED
|
@@ -121,3 +121,9 @@ Run focused tests for a slice and the application's `npm run verify` for a
|
|
|
121
121
|
whole changeset. Use the current-state owners in application operations and
|
|
122
122
|
Playwright for affected UI. There is no current JSKIT Doctor command; ignore
|
|
123
123
|
old CLI authoring-history diagnoses. Never expose environment values.
|
|
124
|
+
|
|
125
|
+
Tests and requests take the time their work needs, no more: a list reads each
|
|
126
|
+
table once, a row policy is bounded by the rows it reads, list and batch paths
|
|
127
|
+
have a query budget tested at realistic row counts, and test databases restore
|
|
128
|
+
a stored snapshot instead of replaying migrations. Find why a check is slow;
|
|
129
|
+
never raise its time limit. Typed features' repository and Tests rules say how.
|
|
@@ -42,9 +42,9 @@ How to use it:
|
|
|
42
42
|
- [server-search.md](./server-search.md)
|
|
43
43
|
- `actualField`, `storage.writeSerializer`, `storage.virtual`, computed field, virtual field, projection, `createCrudResourceRuntime`, datetime write serialization, temporalPrecision, json-rest-schema dates
|
|
44
44
|
- [crud-repository-mapping.md](./crud-repository-mapping.md)
|
|
45
|
-
- row policy, permission filtering, visibility before pagination, recursive CTE, descendants, package cycle, `RowPolicyPlugin
|
|
45
|
+
- row policy, permission filtering, visibility before pagination, recursive CTE, descendants, package cycle, `RowPolicyPlugin`, row policy cost, slow writes through a policy, access check per history item
|
|
46
46
|
- [row-policies.md](./row-policies.md)
|
|
47
|
-
- typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `api.resources.x.query`, every row, list stops at 20 rows, `queryAllJsonRestCollectionRows`, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size
|
|
47
|
+
- typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `api.resources.x.query`, every row, list stops at 20 rows, `queryAllJsonRestCollectionRows`, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size, read per row, N+1 queries, slow endpoint, query budget, realistic row counts, slow tests, raised timeout, `migrate.latest()` in tests, test database snapshot
|
|
48
48
|
- [typed-features.md](./typed-features.md)
|
|
49
49
|
- client features, composable, `use<Name>`, Vue component, client provider, props, emits, `inject`, `provide`, `ref`, `reactive`, `watch`, `addEventListener`, `setInterval`, module state, `Object.freeze` on a composable, `?.()` in a template, `jskit/typed-inject`, `jskit/collaborator-props`, `jskit/fixed-result-shape`, `jskit/released-listeners`, `jskit/no-module-state`, `jskit/no-frozen-vue-state`, `clientFeaturesConfig`, `shared resource:`
|
|
50
50
|
- [client-features.md](./client-features.md)
|
|
@@ -8,6 +8,7 @@ Use when:
|
|
|
8
8
|
- filtering after pagination produces empty or incomplete pages
|
|
9
9
|
- another package grants access to a resource but a reverse dependency would create a cycle
|
|
10
10
|
- the user mentions `RowPolicyPlugin`, recursive CTEs, descendants, or permission-filtered counts
|
|
11
|
+
- reads or writes of a resource with a policy get slower as records or a person's history grow
|
|
11
12
|
|
|
12
13
|
Read first:
|
|
13
14
|
|
|
@@ -36,6 +37,13 @@ Package composition:
|
|
|
36
37
|
- the owner seals the contributions and builds the policy during `boot()`
|
|
37
38
|
- do not use module-global mutable registries
|
|
38
39
|
|
|
40
|
+
Cost:
|
|
41
|
+
|
|
42
|
+
- a policy's cost follows the rows its query reads: add a join, `EXISTS` or subquery correlated with the row through `column()` and `value()`, on indexed columns
|
|
43
|
+
- never load the subject's whole history (every source, notice or grant they were ever linked to) to build an `IN` list, and never call another owner's access check per item of it; another package's grant is a predicate contribution
|
|
44
|
+
- the policy runs on every single-record read, including the read-back after `createDocument()` and `patchDocumentById()` (`returning: "full"`) and the lookup before `patch` and `delete`, so a write pays it once per row it writes
|
|
45
|
+
- a write decides access once, inside its transaction; a batch write that would pay an expensive read-back per row follows the typed features guide's [Bounded access checks](../typed-features.md#bounded-access-checks)
|
|
46
|
+
|
|
39
47
|
Child-resource rules:
|
|
40
48
|
|
|
41
49
|
- a self-referencing relationship targets the same resource and receives the same row policy
|
|
@@ -50,6 +58,7 @@ Testing:
|
|
|
50
58
|
- assert hidden direct records behave as not found
|
|
51
59
|
- assert children and includes do not leak records
|
|
52
60
|
- assert package and provider dependencies stay one-way
|
|
61
|
+
- run one read and one write for a subject with a short history and with a realistic long one (hundreds of linked rows), and assert the same query count
|
|
53
62
|
|
|
54
63
|
AnyAPI boundary:
|
|
55
64
|
|
|
@@ -65,3 +74,4 @@ Avoid:
|
|
|
65
74
|
- adding a default policy file to every CRUD
|
|
66
75
|
- returning a Knex builder or promise from a synchronous grouped contribution
|
|
67
76
|
- swallowing a policy error and retrying without mandatory visibility
|
|
77
|
+
- a policy that reads the subject's history or calls an access check per history item
|
|
@@ -65,6 +65,10 @@ ends with the same link.
|
|
|
65
65
|
- [One reader per fact](../typed-features.md#one-reader-per-fact)
|
|
66
66
|
- `api.resources.x.query(…)` with no `page` for a list, `queryDocuments(query)` with no `limit` in a service, a list that stops at 20 rows
|
|
67
67
|
- [Every row](../typed-features.md#every-row)
|
|
68
|
+
- `await getDocumentById(…)`, `queryDocuments(…)` or a service `get` inside a loop or a `Promise.all` over a list, a single-record helper reused for a list, N+1 queries, a slow list endpoint
|
|
69
|
+
- [One read per table](../typed-features.md#one-read-per-table)
|
|
70
|
+
- a `rowPolicy` that loads a person's whole history or calls an access check per item, `createDocument` per row of a batch, a write that gets slower as history grows
|
|
71
|
+
- [Bounded access checks](../typed-features.md#bounded-access-checks)
|
|
68
72
|
- a route that queries storage, `boot*.js` holding routes
|
|
69
73
|
- [Routes](../typed-features.md#routes)
|
|
70
74
|
- `new AppError(status, message)` with no code, `new NotFoundError("…")`, one code for two sentences (`jskit/app-error-code`)
|
|
@@ -81,6 +85,12 @@ ends with the same link.
|
|
|
81
85
|
- [Tests are not callers](../typed-features.md#tests-are-not-callers)
|
|
82
86
|
- `…ForTests`, `__reset…`, `testOnly…` exports (`jskit/no-test-only-export`)
|
|
83
87
|
- [No test-only exports](../typed-features.md#no-test-only-exports)
|
|
88
|
+
- a list, batch or write test with one to three rows, no query count, a slow endpoint first seen with real data
|
|
89
|
+
- [Budgets at realistic counts](../typed-features.md#budgets-at-realistic-counts)
|
|
90
|
+
- a raised `{ timeout }`, `test.setTimeout()`, `--test-timeout` or Playwright `timeout`, retries or a request loop that absorbs slowness, a slow suite
|
|
91
|
+
- [Time limits are not fixes](../typed-features.md#time-limits-are-not-fixes)
|
|
92
|
+
- `knex.migrate.latest()` in each test file, a template database rebuilt by migrating, test preparation slower than its tests
|
|
93
|
+
- [Test databases are prepared once](../typed-features.md#test-databases-are-prepared-once)
|
|
84
94
|
- a file over 750 lines, a function over 600 (`max-lines`, `max-lines-per-function`), a data module
|
|
85
95
|
- [Size limits](../typed-features.md#size-limits), [Data modules](../typed-features.md#data-modules)
|
|
86
96
|
- two properties on one line in `Object.freeze({ … })` or a token map (`jskit/one-property-per-line`)
|
|
@@ -100,3 +110,5 @@ Avoid:
|
|
|
100
110
|
- module-level registries
|
|
101
111
|
- refusals without their own code
|
|
102
112
|
- excusing a finding without a reason in its `readable-check: allow` comment
|
|
113
|
+
- a read per row of a list, or an access check sized by stored history
|
|
114
|
+
- raising a time limit instead of finding why a check is slow
|
|
@@ -65,11 +65,15 @@ in the regression: one test's teardown must not erase another test's migrations.
|
|
|
65
65
|
|
|
66
66
|
Do not drop/recreate the database, replay completed migrations, build every
|
|
67
67
|
module fixture, or run a production frontend build for each focused check.
|
|
68
|
-
Choose small fixture sets for the selected suite
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
Choose small fixture sets for the selected suite; realistic row counts belong
|
|
69
|
+
in the database tests that hold each endpoint's query budget. Fresh-schema
|
|
70
|
+
migration tests and production-artifact tests are separate operations. Measure
|
|
71
|
+
migration, fixture, and server startup time and fix the slow phase; never retry
|
|
72
|
+
a slow launch, or raise its deadline or a spec's timeout, to absorb it. A page
|
|
73
|
+
that needs a longer timeout to load is a slow endpoint to fix (typed features'
|
|
74
|
+
Time limits are not fixes). When a host supplies a managed target, use its
|
|
75
|
+
scoped suite command so one startup serves the whole batch and cleanup
|
|
76
|
+
restores normal Preview.
|
|
73
77
|
|
|
74
78
|
## Keep browser evidence focused
|
|
75
79
|
|
|
@@ -107,7 +107,7 @@ Exactly this, and nothing more:
|
|
|
107
107
|
| `nav/history` | `history.back()`, `history.go()`, `history.pushState()`/`replaceState()`, `router.back()`, `router.go()` |
|
|
108
108
|
| `nav/window-location` | `window.location` assignments, `.href =`, `location.assign()`, `location.replace()` |
|
|
109
109
|
| `nav/raw-link` | a hand-built in-app path in `to`, `href`, `router.push()`, `router.replace()` or a `to:`/`href:` object key outside a placement |
|
|
110
|
-
| `nav/back-control` | any back control but the shell's Up: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component, a `goBack()`-style handler |
|
|
110
|
+
| `nav/back-control` | any back control but the shell's Up navigating the app: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component, a `goBack()`-style handler |
|
|
111
111
|
| `nav/missing-parent` | an inner page of a shell surface without a declared parent |
|
|
112
112
|
| `nav/top-level-parent` | a main-navigation destination that declares a parent |
|
|
113
113
|
| `nav/allow` | any `m3-check: allow nav/...` comment: nothing excuses a navigation rule |
|
|
@@ -140,6 +140,9 @@ Exactly this, and nothing more:
|
|
|
140
140
|
`finish(recordUrl)`; the record shows, and Up goes where the record's Up went
|
|
141
141
|
before. Wrong: `router.push(recordUrl)` after Save, so Up or Back reopens the
|
|
142
142
|
form.
|
|
143
|
+
- **Steps inside a page are named actions:** "Previous step" or the
|
|
144
|
+
destination's name, not "Back" or a back arrow. A frame's own Back
|
|
145
|
+
(`contentWindow.history`) is fine.
|
|
143
146
|
|
|
144
147
|
## Foundation
|
|
145
148
|
|
|
@@ -191,6 +194,9 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
|
|
|
191
194
|
standalone heading. A section inside a page uses a title role.
|
|
192
195
|
- **Up** sits in the bar on every inner page (see Navigation and Up). On phones
|
|
193
196
|
it replaces the menu icon.
|
|
197
|
+
- **A layout with its own top app bar** calls `useShellAppBar()` and renders
|
|
198
|
+
`ShellUpButton`, `ShellPageTitle` and `ShellPageActionsTarget` in it;
|
|
199
|
+
`jskit check` treats it as a shell layout.
|
|
194
200
|
- **Actions:** a screen's actions go in the bar inside `ShellPageActions`
|
|
195
201
|
(`@jskit-ai/shell-web/client/components/ShellPageActions`), each a
|
|
196
202
|
`ShellPageAction` with `icon`, `label` (also its accessible name and compact
|
|
@@ -317,7 +323,8 @@ Take the approved route first; none needs an exception:
|
|
|
317
323
|
|
|
318
324
|
- **Brand or partner colours** are theme custom colours, used by name:
|
|
319
325
|
`colors: { partner: "#0A7D3E" }` in `src/theme/index.js`, then
|
|
320
|
-
`<v-chip color="partner">`.
|
|
326
|
+
`<v-chip color="partner">`. `{ value: "#F5C518", blend: false }` keeps
|
|
327
|
+
its hue.
|
|
321
328
|
- **Chart and data colours** come from the theme: `const theme = useTheme()`,
|
|
322
329
|
then `["primary", "tertiary"].map((role) => theme.current.value.colors[role])`.
|
|
323
330
|
- **A third-party widget** lives in one wrapper component, such as
|
|
@@ -15,7 +15,7 @@ A concrete Fastify, Vue, and JSKIT application foundation for products that do n
|
|
|
15
15
|
|
|
16
16
|
- Id: `app/minimal-foundation`
|
|
17
17
|
- Keywords: `app`, `fastify`, `foundation`, `minimal`, `server`, `vite`, `vue`
|
|
18
|
-
- Owner: `@jskit-ai/agent-docs@0.1.
|
|
18
|
+
- Owner: `@jskit-ai/agent-docs@0.1.277`
|
|
19
19
|
- Read: [PATTERN.md](patterns/app/minimal-foundation/PATTERN.md)
|
|
20
20
|
- Examples: [example/](patterns/app/minimal-foundation/example/)
|
|
21
21
|
- Requires: `@jskit-ai/http-runtime`, `@jskit-ai/kernel`
|
|
@@ -26,7 +26,7 @@ A concrete JSKIT web application foundation with responsive shell navigation, se
|
|
|
26
26
|
|
|
27
27
|
- Id: `app/shell-foundation`
|
|
28
28
|
- Keywords: `app`, `foundation`, `material`, `navigation`, `placements`, `shell`, `vite`, `vue`
|
|
29
|
-
- Owner: `@jskit-ai/agent-docs@0.1.
|
|
29
|
+
- Owner: `@jskit-ai/agent-docs@0.1.277`
|
|
30
30
|
- Read: [PATTERN.md](patterns/app/shell-foundation/PATTERN.md)
|
|
31
31
|
- Examples: [example/](patterns/app/shell-foundation/example/)
|
|
32
32
|
- Requires: `@jskit-ai/http-runtime`, `@jskit-ai/kernel`, `@jskit-ai/shell-web`
|
|
@@ -37,7 +37,7 @@ Configure an assistant runtime for one application surface and expose its chat a
|
|
|
37
37
|
|
|
38
38
|
- Id: `assistant/assistant-surface`
|
|
39
39
|
- Keywords: `ai`, `assistant`, `chat`, `config`, `environment`, `page`, `placement`, `settings`, `surface`
|
|
40
|
-
- Owner: `@jskit-ai/assistant-runtime@0.2.
|
|
40
|
+
- Owner: `@jskit-ai/assistant-runtime@0.2.25`
|
|
41
41
|
- Read: [PATTERN.md](patterns/assistant/assistant-surface/PATTERN.md)
|
|
42
42
|
- Examples: [example/](patterns/assistant/assistant-surface/example/)
|
|
43
43
|
- Requires: `@jskit-ai/assistant-runtime`, `@jskit-ai/connectors-catalog`, `@jskit-ai/connectors-core`, `@jskit-ai/shell-web`
|
|
@@ -59,7 +59,7 @@ Compose JSKIT authentication routes, views, profile controls, and public surface
|
|
|
59
59
|
|
|
60
60
|
- Id: `auth/auth-surface`
|
|
61
61
|
- Keywords: `account`, `auth`, `login`, `logout`, `password`, `placement`, `profile`, `reset`, `surface`
|
|
62
|
-
- Owner: `@jskit-ai/auth-web@0.2.
|
|
62
|
+
- Owner: `@jskit-ai/auth-web@0.2.14`
|
|
63
63
|
- Read: [PATTERN.md](patterns/auth/auth-surface/PATTERN.md)
|
|
64
64
|
- Examples: [example/](patterns/auth/auth-surface/example/)
|
|
65
65
|
- Requires: `@jskit-ai/auth-core`, `@jskit-ai/auth-web`, `@jskit-ai/shell-web`
|
|
@@ -70,7 +70,7 @@ Let people sign in with the accounts they already have in an accounts hub or ano
|
|
|
70
70
|
|
|
71
71
|
- Id: `auth/oidc-sign-in`
|
|
72
72
|
- Keywords: `accounts hub`, `auth`, `authentication`, `back-channel logout`, `dynamic client registration`, `issuer`, `master login`, `oidc`, `openid connect`, `single sign-on`, `sso`
|
|
73
|
-
- Owner: `@jskit-ai/auth-provider-oidc-core@0.2.
|
|
73
|
+
- Owner: `@jskit-ai/auth-provider-oidc-core@0.2.16`
|
|
74
74
|
- Read: [PATTERN.md](patterns/auth/oidc-sign-in/PATTERN.md)
|
|
75
75
|
- Examples: [example/](patterns/auth/oidc-sign-in/example/)
|
|
76
76
|
- Requires: `@jskit-ai/auth-provider-oidc-core`, `@jskit-ai/auth-web`
|
|
@@ -246,7 +246,7 @@ Add a protected administration surface, settings shell, and semantic navigation
|
|
|
246
246
|
|
|
247
247
|
- Id: `console/console-surface`
|
|
248
248
|
- Keywords: `admin`, `console`, `navigation`, `owner`, `placement`, `settings`, `surface`
|
|
249
|
-
- Owner: `@jskit-ai/console-web@0.1.
|
|
249
|
+
- Owner: `@jskit-ai/console-web@0.1.236`
|
|
250
250
|
- Read: [PATTERN.md](patterns/console/console-surface/PATTERN.md)
|
|
251
251
|
- Examples: [example/](patterns/console/console-surface/example/)
|
|
252
252
|
- Requires: `@jskit-ai/console-core`, `@jskit-ai/console-web`, `@jskit-ai/shell-web`
|
|
@@ -257,7 +257,7 @@ Build a routed CRUD user interface as thin application pages over JSKIT's shared
|
|
|
257
257
|
|
|
258
258
|
- Id: `crud/crud-screen-set`
|
|
259
259
|
- Keywords: `actions`, `add`, `crud`, `delete`, `edit`, `filters`, `list`, `material`, `routes`, `view`, `vue`
|
|
260
|
-
- Owner: `@jskit-ai/http-web@0.2.
|
|
260
|
+
- Owner: `@jskit-ai/http-web@0.2.17`
|
|
261
261
|
- Read: [PATTERN.md](patterns/crud/crud-screen-set/PATTERN.md)
|
|
262
262
|
- Examples: [example/](patterns/crud/crud-screen-set/example/)
|
|
263
263
|
- Requires: `@jskit-ai/http-web`, `@jskit-ai/resource-crud-core`
|
|
@@ -312,7 +312,7 @@ Wrap a JSKIT web application in a Capacitor Android shell using native Capacitor
|
|
|
312
312
|
|
|
313
313
|
- Id: `mobile/android-application`
|
|
314
314
|
- Keywords: `android`, `capacitor`, `device`, `mobile`, `native`, `shell`, `webview`
|
|
315
|
-
- Owner: `@jskit-ai/mobile-capacitor@0.2.
|
|
315
|
+
- Owner: `@jskit-ai/mobile-capacitor@0.2.12`
|
|
316
316
|
- Read: [PATTERN.md](patterns/mobile/android-application/PATTERN.md)
|
|
317
317
|
- Examples: [example/](patterns/mobile/android-application/example/)
|
|
318
318
|
- Requires: `@capacitor/android`, `@capacitor/app`, `@capacitor/cli`, `@jskit-ai/mobile-capacitor`
|
|
@@ -323,7 +323,7 @@ Add JSKIT realtime events with an optional Redis backplane and an explicit shell
|
|
|
323
323
|
|
|
324
324
|
- Id: `realtime/realtime-application`
|
|
325
325
|
- Keywords: `realtime`, `redis`, `socket.io`, `sockets`, `status`, `websocket`
|
|
326
|
-
- Owner: `@jskit-ai/realtime@0.2.
|
|
326
|
+
- Owner: `@jskit-ai/realtime@0.2.15`
|
|
327
327
|
- Read: [PATTERN.md](patterns/realtime/realtime-application/PATTERN.md)
|
|
328
328
|
- Examples: [example/](patterns/realtime/realtime-application/example/)
|
|
329
329
|
- Requires: `@jskit-ai/realtime`, `@jskit-ai/shell-web`
|
|
@@ -334,7 +334,7 @@ Compose application-owned Google Publisher Tag delivery with the generic rewarde
|
|
|
334
334
|
|
|
335
335
|
- Id: `rewards/google-rewarded`
|
|
336
336
|
- Keywords: `ads`, `google`, `publisher tag`, `rewarded`, `unlock`
|
|
337
|
-
- Owner: `@jskit-ai/rewarded-web@0.2.
|
|
337
|
+
- Owner: `@jskit-ai/rewarded-web@0.2.14`
|
|
338
338
|
- Read: [PATTERN.md](patterns/rewards/google-rewarded/PATTERN.md)
|
|
339
339
|
- Examples: [example/](patterns/rewards/google-rewarded/example/)
|
|
340
340
|
- Requires: `@jskit-ai/rewarded-web`
|
|
@@ -345,7 +345,7 @@ Define a server feature through explicit capabilities and first-class actions, a
|
|
|
345
345
|
|
|
346
346
|
- Id: `server/feature-package`
|
|
347
347
|
- Keywords: `actions`, `feature`, `json-rest`, `knex`, `orchestration`, `package`, `provider`, `repository`, `routes`, `server`
|
|
348
|
-
- Owner: `@jskit-ai/agent-docs@0.1.
|
|
348
|
+
- Owner: `@jskit-ai/agent-docs@0.1.277`
|
|
349
349
|
- Read: [PATTERN.md](patterns/server/feature-package/PATTERN.md)
|
|
350
350
|
- Examples: [example/](patterns/server/feature-package/example/)
|
|
351
351
|
- Requires: `@jskit-ai/kernel`
|
|
@@ -356,7 +356,7 @@ Compose the JSKIT responsive shell, semantic placements, settings navigation, an
|
|
|
356
356
|
|
|
357
357
|
- Id: `shell/application-shell`
|
|
358
358
|
- Keywords: `adaptive`, `app`, `layout`, `navigation`, `placement`, `responsive`, `settings`, `shell`
|
|
359
|
-
- Owner: `@jskit-ai/shell-web@0.2.
|
|
359
|
+
- Owner: `@jskit-ai/shell-web@0.2.12`
|
|
360
360
|
- Read: [PATTERN.md](patterns/shell/application-shell/PATTERN.md)
|
|
361
361
|
- Examples: [example/](patterns/shell/application-shell/example/)
|
|
362
362
|
- Requires: `@jskit-ai/kernel`, `@jskit-ai/shell-web`
|
|
@@ -367,7 +367,7 @@ Add product routes and shell extensions through file routing, semantic placement
|
|
|
367
367
|
|
|
368
368
|
- Id: `ui/page-and-placement`
|
|
369
369
|
- Keywords: `component`, `navigation`, `outlet`, `page`, `placement`, `routes`, `section`, `shell`, `subpages`, `vue`
|
|
370
|
-
- Owner: `@jskit-ai/shell-web@0.2.
|
|
370
|
+
- Owner: `@jskit-ai/shell-web@0.2.12`
|
|
371
371
|
- Read: [PATTERN.md](patterns/ui/page-and-placement/PATTERN.md)
|
|
372
372
|
- Examples: [example/](patterns/ui/page-and-placement/example/)
|
|
373
373
|
- Requires: `@jskit-ai/kernel`, `@jskit-ai/shell-web`
|
|
@@ -378,7 +378,7 @@ Compose an account settings route and profile, preference, notification, and sig
|
|
|
378
378
|
|
|
379
379
|
- Id: `users/account-settings`
|
|
380
380
|
- Keywords: `account`, `link provider`, `notifications`, `oauth`, `preferences`, `profile`, `settings`, `sign-in methods`, `user`, `vue`
|
|
381
|
-
- Owner: `@jskit-ai/users-web@0.2.
|
|
381
|
+
- Owner: `@jskit-ai/users-web@0.2.19`
|
|
382
382
|
- Read: [PATTERN.md](patterns/users/account-settings/PATTERN.md)
|
|
383
383
|
- Examples: [example/](patterns/users/account-settings/example/)
|
|
384
384
|
- Requires: `@jskit-ai/shell-web`, `@jskit-ai/users-core`, `@jskit-ai/users-web`
|
|
@@ -411,7 +411,7 @@ Compose workspace selection, invitation, member administration, settings, and re
|
|
|
411
411
|
|
|
412
412
|
- Id: `workspaces/workspace-surfaces`
|
|
413
413
|
- Keywords: `admin`, `invite`, `member`, `navigation`, `settings`, `surface`, `switcher`, `workspace`
|
|
414
|
-
- Owner: `@jskit-ai/workspaces-web@0.2.
|
|
414
|
+
- Owner: `@jskit-ai/workspaces-web@0.2.19`
|
|
415
415
|
- Read: [PATTERN.md](patterns/workspaces/workspace-surfaces/PATTERN.md)
|
|
416
416
|
- Examples: [example/](patterns/workspaces/workspace-surfaces/example/)
|
|
417
417
|
- Requires: `@jskit-ai/shell-web`, `@jskit-ai/workspaces-core`, `@jskit-ai/workspaces-web`
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
37
|
"@jskit-ai/kernel": "0.2.10",
|
|
38
|
-
"@jskit-ai/material-web": "0.1.
|
|
38
|
+
"@jskit-ai/material-web": "0.1.1",
|
|
39
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
40
40
|
"fastify": "^5.12.7",
|
|
41
41
|
"pino": "^10.3.1",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jskit-ai/config-eslint": "0.1.245",
|
|
53
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
53
|
+
"@jskit-ai/jskit-catalog": "0.2.29",
|
|
54
54
|
"@playwright/test": "1.61.1",
|
|
55
55
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
56
56
|
"eslint": "^10.8.0",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
37
|
"@jskit-ai/kernel": "0.2.10",
|
|
38
|
-
"@jskit-ai/material-web": "0.1.
|
|
38
|
+
"@jskit-ai/material-web": "0.1.1",
|
|
39
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
40
40
|
"fastify": "^5.12.7",
|
|
41
41
|
"pino": "^10.3.1",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"vue-router": "^5.3.1",
|
|
46
46
|
"vuetify": "^4.3.0",
|
|
47
47
|
"@jskit-ai/http-runtime": "0.2.10",
|
|
48
|
-
"@jskit-ai/shell-web": "0.2.
|
|
48
|
+
"@jskit-ai/shell-web": "0.2.12",
|
|
49
49
|
"@fastify/fast-json-stringify-compiler": "^5.1.0",
|
|
50
50
|
"@vue/devtools-api": "^8.2.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jskit-ai/config-eslint": "0.1.245",
|
|
54
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
54
|
+
"@jskit-ai/jskit-catalog": "0.2.29",
|
|
55
55
|
"@playwright/test": "1.61.1",
|
|
56
56
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
57
57
|
"eslint": "^10.8.0",
|
|
@@ -719,6 +719,107 @@ const rows = extractJsonRestCollectionRows(await api.resources.workspaceMembersh
|
|
|
719
719
|
}, createJsonRestContext(options.context)));
|
|
720
720
|
```
|
|
721
721
|
|
|
722
|
+
### One read per table
|
|
723
|
+
|
|
724
|
+
**A read that builds a list reads each table once for the whole list: one
|
|
725
|
+
query per table with an id-list filter, the rows grouped in memory. It never
|
|
726
|
+
reads one row, or runs one lookup, per item of the list.**
|
|
727
|
+
|
|
728
|
+
- Collect the ids from the first read, then read each related table once with
|
|
729
|
+
an id-list filter: a `searchSchema` entry
|
|
730
|
+
`{ type: "array", actualField: "<column>", filterOperator: "in" }`, read
|
|
731
|
+
through `queryAllJsonRestCollectionRows()`, or the resource's `include`.
|
|
732
|
+
Group the rows by id with `Map.groupBy()`.
|
|
733
|
+
- `getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
|
|
734
|
+
single-record read inside a loop over the list is a read per row, however
|
|
735
|
+
the loop is spelled (`for`, `map` with `Promise.all`, a helper called per
|
|
736
|
+
item). So is a single-record helper reused for a list.
|
|
737
|
+
- Work derived from a row (hashing, preparing a form, assembling a snapshot)
|
|
738
|
+
runs once per distinct input in the call, not once per row it appears in.
|
|
739
|
+
- The list's query count follows its tables and pages (100 rows a page unless
|
|
740
|
+
the resource sets `queryMaxLimit`), never its rows: three hundred invoices
|
|
741
|
+
and their lines take a handful of queries, not three hundred more (see
|
|
742
|
+
[Budgets at realistic counts](#budgets-at-realistic-counts)).
|
|
743
|
+
|
|
744
|
+
Why: a read per row costs nothing with three test rows and seconds with a few
|
|
745
|
+
hundred real ones, and every lookup and visibility check inside it repeats too.
|
|
746
|
+
|
|
747
|
+
```js
|
|
748
|
+
// good: one read per table, however many invoices there are
|
|
749
|
+
const invoiceRows = await queryAllJsonRestCollectionRows(api.resources.invoices, {
|
|
750
|
+
queryParams: { filters: { workspace: workspaceId } },
|
|
751
|
+
transaction: resolveTransaction(options),
|
|
752
|
+
format: "plain"
|
|
753
|
+
}, options.context);
|
|
754
|
+
const lineRows = await queryAllJsonRestCollectionRows(api.resources.invoiceLines, {
|
|
755
|
+
queryParams: { filters: { invoiceIds: invoiceRows.map((invoice) => invoice.id) } },
|
|
756
|
+
transaction: resolveTransaction(options),
|
|
757
|
+
format: "plain"
|
|
758
|
+
}, options.context);
|
|
759
|
+
const linesByInvoice = Map.groupBy(lineRows, (line) => line.invoiceId);
|
|
760
|
+
return invoiceRows.map((invoice) => ({ ...invoice, lines: linesByInvoice.get(invoice.id) || [] }));
|
|
761
|
+
|
|
762
|
+
// bad: one more query, and its lookups and row policy, for every invoice
|
|
763
|
+
for (const invoice of invoiceRows) {
|
|
764
|
+
invoice.lines = await invoiceLines.queryDocuments({ invoiceId: invoice.id }, options);
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Bounded access checks
|
|
769
|
+
|
|
770
|
+
**A row policy costs what the rows its query reads cost, never what the
|
|
771
|
+
subject's stored history costs, and a write decides access once instead of
|
|
772
|
+
paying a read policy for every row it writes.**
|
|
773
|
+
|
|
774
|
+
- A `rowPolicy` adds one predicate to the query it is given: a join, `EXISTS`
|
|
775
|
+
or subquery correlated with the row being read, on indexed columns. It never
|
|
776
|
+
loads everything the subject was ever linked to (every source, notice or
|
|
777
|
+
grant in their history) to build an `IN` list, and never calls another
|
|
778
|
+
owner's access check once per item of that history. The
|
|
779
|
+
[row policies heuristic](heuristics/row-policies.md)
|
|
780
|
+
has the policy contract and its package composition.
|
|
781
|
+
- The policy runs on every single-record read, and a CRUD write includes one:
|
|
782
|
+
`createDocument()` and `patchDocumentById()` write with `returning: "full"`
|
|
783
|
+
and read the record back through the policy, and `patch` and `delete` first
|
|
784
|
+
look the record up through it. A write therefore pays the read policy for
|
|
785
|
+
each row it writes.
|
|
786
|
+
- A write path decides access once, inside its transaction. A batch write (a
|
|
787
|
+
delivery, an import, a fan-out) whose per-row read-back would pay an
|
|
788
|
+
expensive policy writes through a repository method added with
|
|
789
|
+
`decorateRepository`, on `resolveRepoClient(database.knex, options)`,
|
|
790
|
+
setting the same columns the resource would, its ownership columns
|
|
791
|
+
included, and reading back only what it returns.
|
|
792
|
+
- Its test runs one read and one write for a subject with a short history and
|
|
793
|
+
with a realistic long one, and asserts the same query count.
|
|
794
|
+
|
|
795
|
+
Why: a check sized by history grows with every record the subject receives,
|
|
796
|
+
so the same write that takes milliseconds in a test takes seconds a year later.
|
|
797
|
+
|
|
798
|
+
```js
|
|
799
|
+
// good: one predicate on the row being read
|
|
800
|
+
function noticeVisibilityPolicy({ query, context, column, value, db }) {
|
|
801
|
+
const userId = resolveActorId(context);
|
|
802
|
+
if (!userId) return false;
|
|
803
|
+
query.where(column("recipientId"), value("recipientId", userId)).whereExists(
|
|
804
|
+
db("sources").select(1)
|
|
805
|
+
.whereColumn("sources.id", column("sourceId"))
|
|
806
|
+
.where("sources.status", "published")
|
|
807
|
+
);
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// bad: every source the recipient was ever notified about, checked one by one
|
|
812
|
+
async function noticeVisibilityPolicy({ query, context, column, value }) {
|
|
813
|
+
const sourceIds = await noticeHistory.listSourceIds(resolveActorId(context));
|
|
814
|
+
const readable = [];
|
|
815
|
+
for (const sourceId of sourceIds) {
|
|
816
|
+
if (await sources.canRead(sourceId, context)) readable.push(sourceId);
|
|
817
|
+
}
|
|
818
|
+
query.whereIn(column("sourceId"), readable.map((id) => value("sourceId", id)));
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
722
823
|
### Routes
|
|
723
824
|
|
|
724
825
|
**A route does HTTP work only (status, cookies, redirects) and runs an action
|
|
@@ -905,6 +1006,112 @@ export function resetInvoiceCacheForTests() {
|
|
|
905
1006
|
}
|
|
906
1007
|
```
|
|
907
1008
|
|
|
1009
|
+
### Budgets at realistic counts
|
|
1010
|
+
|
|
1011
|
+
**A list, batch or write path has a database test at the row counts and
|
|
1012
|
+
history a real workspace holds, and that test fails above a fixed query
|
|
1013
|
+
budget, one that a read per row or a history-sized check cannot meet.**
|
|
1014
|
+
|
|
1015
|
+
- Seed hundreds of records and long per-person histories, not one to three
|
|
1016
|
+
rows: a read per row and a history-sized policy cost nothing at three.
|
|
1017
|
+
- Count the queries with the test Knex instance's `query` event and assert a
|
|
1018
|
+
ceiling: the count the correct implementation needs at that size, which a
|
|
1019
|
+
read per row overshoots by hundreds. A path that pages nothing (one read,
|
|
1020
|
+
one write) asserts the same count at a short and a long history. A query
|
|
1021
|
+
count is stable in CI; also report the time the call took.
|
|
1022
|
+
- Meet the budget by fixing the reads ([One read per
|
|
1023
|
+
table](#one-read-per-table), [Bounded access checks](#bounded-access-checks)),
|
|
1024
|
+
never by shrinking the data or raising the budget.
|
|
1025
|
+
|
|
1026
|
+
Why: without a budget at real counts, a path that does one read per row ships,
|
|
1027
|
+
and its first sign is a request that takes seconds in production.
|
|
1028
|
+
|
|
1029
|
+
```js
|
|
1030
|
+
// good
|
|
1031
|
+
test("the invoice list reads each table once", async () => {
|
|
1032
|
+
await seedInvoices(knex, { workspace, count: 300, linesEach: 5 });
|
|
1033
|
+
let queries = 0;
|
|
1034
|
+
const count = () => { queries += 1; };
|
|
1035
|
+
knex.on("query", count);
|
|
1036
|
+
try {
|
|
1037
|
+
await invoices.listWithLines(workspace, { context });
|
|
1038
|
+
} finally {
|
|
1039
|
+
knex.off("query", count);
|
|
1040
|
+
}
|
|
1041
|
+
// A page of each table at a time; one read per invoice would add 300.
|
|
1042
|
+
assert.ok(queries <= 40, `${queries} queries`);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// bad: three invoices and no budget, so a read per invoice passes
|
|
1046
|
+
test("the invoice list has its lines", async () => {
|
|
1047
|
+
await seedInvoices(knex, { workspace, count: 3, linesEach: 1 });
|
|
1048
|
+
assert.equal((await invoices.listWithLines(workspace, { context })).length, 3);
|
|
1049
|
+
});
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### Time limits are not fixes
|
|
1053
|
+
|
|
1054
|
+
**A slow test, suite, preparation or request is a defect to find, never a time
|
|
1055
|
+
limit to raise. Measure where the time goes, fix that cause, and keep the
|
|
1056
|
+
default limits.**
|
|
1057
|
+
|
|
1058
|
+
- Raising `--test-timeout`, a test's `{ timeout }`, Playwright's `timeout`,
|
|
1059
|
+
`test.setTimeout()` or an `expect` timeout, adding retries, or calling a
|
|
1060
|
+
request in a loop until it answers hides the cause and lets it grow.
|
|
1061
|
+
- Read the preparation phases' durations and the query counts first; the
|
|
1062
|
+
cause is usually a read per row, a history-sized policy, or a database
|
|
1063
|
+
prepared by replaying migrations ([Test databases are prepared
|
|
1064
|
+
once](#test-databases-are-prepared-once)).
|
|
1065
|
+
- A longer limit is right only for work that is long by nature, such as a
|
|
1066
|
+
fixture of deliberate size, and the test names that work.
|
|
1067
|
+
- A time limit still bounds every run, so a hang ends; it is not a speed
|
|
1068
|
+
setting.
|
|
1069
|
+
|
|
1070
|
+
Why: a raised limit absorbs today's slowness and every later one, until a run
|
|
1071
|
+
takes hours and nobody knows which change made it slow.
|
|
1072
|
+
|
|
1073
|
+
```js
|
|
1074
|
+
// good: the slow step was found and fixed, so the default limit holds
|
|
1075
|
+
test("delivers a batch of notices", async () => {
|
|
1076
|
+
await deliveries.processBatch(workspace, { context });
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// bad
|
|
1080
|
+
test("delivers a batch of notices", { timeout: 600_000 }, async () => {
|
|
1081
|
+
await deliveries.processBatch(workspace, { context });
|
|
1082
|
+
});
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
### Test databases are prepared once
|
|
1086
|
+
|
|
1087
|
+
**A test run prepares each database once, from a stored snapshot of its
|
|
1088
|
+
schema, migration ledger and seed rows checked by a digest of their sources,
|
|
1089
|
+
and never replays the whole migration history inside the run.**
|
|
1090
|
+
|
|
1091
|
+
- The snapshot is rebuilt only when the digest of the migration and seed
|
|
1092
|
+
sources changes, once and outside the timed tests; every disposable
|
|
1093
|
+
database, and every database a test migrated up or down, is restored from
|
|
1094
|
+
it.
|
|
1095
|
+
- The full migration history runs once, in its own fresh-schema check
|
|
1096
|
+
([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
|
|
1097
|
+
- The harness reports each preparation phase and its duration, and the suite
|
|
1098
|
+
has a time budget, so slow preparation shows instead of hiding inside the
|
|
1099
|
+
next test that waits for it.
|
|
1100
|
+
- The database layer guide's [integration test
|
|
1101
|
+
databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)
|
|
1102
|
+
gives the snapshot's contents per engine.
|
|
1103
|
+
|
|
1104
|
+
Why: the migration history only grows, so a run that replays it whenever a
|
|
1105
|
+
schema changed gets slower with every migration the application adds.
|
|
1106
|
+
|
|
1107
|
+
```js
|
|
1108
|
+
// good: the application's test support restores the snapshot its digest names
|
|
1109
|
+
before(async () => restoreTestDatabase(knex, await testDatabaseSnapshot()));
|
|
1110
|
+
|
|
1111
|
+
// bad: every file replays every migration
|
|
1112
|
+
before(() => knex.migrate.latest());
|
|
1113
|
+
```
|
|
1114
|
+
|
|
908
1115
|
## Files and layout
|
|
909
1116
|
|
|
910
1117
|
### Size limits
|