@jskit-ai/agent-docs 0.1.283 → 0.1.284
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guide/agent/app-extras/assistant.md +14 -2
- package/guide/agent/app-extras/realtime.md +38 -1
- package/guide/agent/app-setup/authentication.md +64 -14
- package/guide/agent/app-setup/console.md +8 -0
- package/guide/agent/app-setup/database-layer.md +23 -103
- package/guide/agent/app-setup/existing-application-migration.md +105 -72
- package/guide/agent/app-setup/multi-homing.md +94 -32
- package/guide/agent/app-setup/quickstart.md +1 -1
- package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
- package/guide/agent/app-setup/upgrading-jskit.md +8 -3
- package/guide/agent/app-setup/users.md +8 -5
- package/guide/agent/framework/application-operations.md +1 -1
- package/guide/agent/framework/crud-operations.md +63 -45
- package/guide/agent/framework/data-access.md +1031 -0
- package/guide/agent/framework/sanity-checks.md +26 -27
- package/guide/agent/framework/typed-features.md +114 -247
- package/guide/agent/index.md +2 -0
- package/package.json +6 -3
- package/patterns/feature-package/PATTERN.md +25 -17
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
- package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/patterns/feature-package/example/variations/types.js +1 -1
- package/patterns/minimal-foundation/example/AGENTS.md +4 -0
- package/patterns/minimal-foundation/example/package.json +4 -4
- package/patterns/minimal-foundation/example/server.js +4 -1
- package/patterns/shell-foundation/example/AGENTS.md +4 -0
- package/patterns/shell-foundation/example/package.json +5 -5
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/server.js +4 -1
- package/reference/autogen/KERNEL_MAP.md +2 -1
- package/reference/autogen/PATTERN_INDEX.md +70 -59
- package/reference/autogen/README.md +1 -1
- package/reference/autogen/packages/agent-docs.md +7 -3
- package/reference/autogen/packages/agent-runtime.md +48 -11
- package/reference/autogen/packages/architecture-inspector.md +6 -2
- package/reference/autogen/packages/assistant-core.md +4 -4
- package/reference/autogen/packages/assistant-runtime.md +33 -13
- package/reference/autogen/packages/auth-provider-local-core.md +3 -1
- package/reference/autogen/packages/auth-provider-local-db-core.md +65 -14
- package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
- package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
- package/reference/autogen/packages/connectors-core.md +22 -2
- package/reference/autogen/packages/console-core.md +9 -5
- package/reference/autogen/packages/crud-core.md +38 -173
- package/reference/autogen/packages/database-runtime.md +8 -32
- package/reference/autogen/packages/http-runtime.md +3 -3
- package/reference/autogen/packages/http-web.md +12 -0
- package/reference/autogen/packages/kernel.md +25 -2
- package/reference/autogen/packages/oidc-issuer-core.md +4 -0
- package/reference/autogen/packages/payments-core.md +29 -6
- package/reference/autogen/packages/realtime.md +15 -6
- package/reference/autogen/packages/resource-core.md +30 -5
- package/reference/autogen/packages/resource-crud-core.md +30 -34
- package/reference/autogen/packages/resource-runtime.md +198 -0
- package/reference/autogen/packages/rewarded-core.md +16 -10
- package/reference/autogen/packages/shell-web.md +1 -0
- package/reference/autogen/packages/users-core.md +26 -14
- package/reference/autogen/packages/workspaces-core.md +17 -10
- package/reference/autogen/tooling/config-eslint.md +69 -0
- package/reference/autogen/tooling/jskit-catalog.md +36 -1
- package/reference/autogen/tooling/jskit-release.md +3 -0
- package/skills/jskit/SKILL.md +4 -1
- package/skills/jskit/references/app-operations.md +1 -1
- package/skills/jskit/references/crud-operations.md +62 -44
- package/skills/jskit/references/data-access.md +1026 -0
- package/skills/jskit/references/database-layer.md +411 -0
- package/skills/jskit/references/existing-application-migration.md +105 -72
- package/skills/jskit/references/heuristics/INDEX.md +6 -4
- package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
- package/skills/jskit/references/heuristics/data-access.md +19 -0
- package/skills/jskit/references/heuristics/filters.md +6 -6
- package/skills/jskit/references/heuristics/row-policies.md +40 -16
- package/skills/jskit/references/heuristics/server-search.md +30 -42
- package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
- package/skills/jskit/references/heuristics/typed-features.md +3 -3
- package/skills/jskit/references/multi-homing.md +94 -32
- package/skills/jskit/references/pattern-index.md +70 -59
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
- package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
- package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +117 -0
- package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +59 -10
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
- package/skills/jskit/references/sanity-checks.md +27 -28
- package/skills/jskit/references/typed-features.md +114 -247
- package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
- package/reference/autogen/packages/json-rest-api-core.md +0 -66
- package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
- package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
- package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
- package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
- /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
- /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
# Sanity checks
|
|
4
4
|
|
|
5
5
|
Before calling a list, a save, a test, a refactor, a screen or a deployment
|
|
6
|
-
done, measure what it
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
done, measure what it costs and does, judge whether that is sane, and fix the
|
|
7
|
+
cause when it is not. This page holds JSKIT's part: the pitfalls JSKIT's own
|
|
8
|
+
packages cause, then the JSKIT call, helper or setting that applies each
|
|
9
|
+
check, with one line on what it catches and why.
|
|
10
10
|
|
|
11
11
|
## JSKIT's own pitfalls
|
|
12
12
|
|
|
@@ -66,36 +66,35 @@ expects the relationship id or an unselected field never gets it.
|
|
|
66
66
|
|
|
67
67
|
### Every row
|
|
68
68
|
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- A lookup by a unique key matches at most one row, so it may read `rows[0]`
|
|
77
|
-
of a plain query.
|
|
69
|
+
- A handle's `query()` answers one page and refuses a missing or oversized
|
|
70
|
+
`page.size`, because json-rest-api would otherwise cut the list without an
|
|
71
|
+
error. A read its callers treat as complete uses `queryAll()`, because the
|
|
72
|
+
missing rows otherwise vanish from every picker and delivery built on it
|
|
73
|
+
([Reads](/guide/framework/data-access#reads-shaped-for-the-screen)).
|
|
74
|
+
- A lookup by a unique key matches at most one row: `get(id)`, or a `query()`
|
|
75
|
+
of page size 1.
|
|
78
76
|
|
|
79
77
|
### Query counts and batches
|
|
80
78
|
|
|
81
|
-
- Count a path's queries with `
|
|
82
|
-
`@jskit-ai/
|
|
79
|
+
- Count a path's queries with `expectFlatQueries()` from
|
|
80
|
+
`@jskit-ai/resource-runtime/test`, or `countQueries()` from
|
|
81
|
+
`@jskit-ai/database-runtime/server/testDatabases`, at a few rows and at a
|
|
83
82
|
realistic number; the count should follow tables and pages, not rows
|
|
84
|
-
([
|
|
85
|
-
sizes](/guide/framework/typed-features#query-counts-at-realistic-sizes)).
|
|
83
|
+
([Tests](/guide/framework/data-access#tests)).
|
|
86
84
|
- A JSON:API read adds a few lookup and visibility queries per page and per
|
|
87
|
-
relationship field
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
relationship field, and json-rest-api reads each relationship's related ids
|
|
86
|
+
in one statement however many rows the page has. Both follow pages, not
|
|
87
|
+
rows, so `expectFlatQueries` passes them.
|
|
88
|
+
- A handle's `get()` or `query()`, or a service `get`, inside a loop is the
|
|
89
|
+
usual per-row read; batch it with an id-list filter through `queryAll()` or
|
|
90
|
+
`include`, grouped with `Map.groupBy()`
|
|
91
|
+
([Reads](/guide/framework/data-access#reads-shaped-for-the-screen)).
|
|
92
92
|
- A `rowPolicy` adds one correlated predicate to the query it is given, so
|
|
93
93
|
its cost follows the rows read, not the subject's history. It runs on every
|
|
94
|
-
single-record read, including a write's default `returning: "
|
|
95
|
-
back, so a write that discards its result passes `returning: "
|
|
96
|
-
a batch
|
|
97
|
-
([
|
|
98
|
-
checks](/guide/framework/typed-features#bounded-access-checks)).
|
|
94
|
+
single-record read, including a write's default `returning: "record"` read
|
|
95
|
+
back, so a write that discards its result passes `returning: "id"` or
|
|
96
|
+
`"none"`, and a batch uses the many-row writes (`createMany()`,
|
|
97
|
+
`patchMany()`) ([Layer 1](/guide/framework/data-access#layer-1-the-table-resource)).
|
|
99
98
|
|
|
100
99
|
### Tests
|
|
101
100
|
|
|
@@ -13,6 +13,9 @@ what to do, why in one line, and shows good and bad code. `npm run lint`
|
|
|
13
13
|
reports the rules a machine can see ([checks](#checks)); the others are
|
|
14
14
|
checked by reading. The `server/feature-package` pattern in the
|
|
15
15
|
[source pattern library](/patterns/) is the smallest complete feature to copy.
|
|
16
|
+
Code that reads or writes a table starts from [data access](/guide/framework/data-access):
|
|
17
|
+
the table resource and its owner feature come first, and this page shapes the
|
|
18
|
+
code around them.
|
|
16
19
|
|
|
17
20
|
The [typed features heuristic](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/heuristics/typed-features.md)
|
|
18
21
|
maps keywords and lint findings to the section that fixes them. Client code
|
|
@@ -44,11 +47,13 @@ it reports the call as exact instead of guessing from the method name.
|
|
|
44
47
|
### Feature files
|
|
45
48
|
|
|
46
49
|
**A `defineFeature` lives in `<Name>Feature.js`, its keys in one order, one
|
|
47
|
-
per line: `id, domain,
|
|
48
|
-
shutdown`.** A `defineProvider` lives in `<Name>Provider.js`.
|
|
50
|
+
per line: `id, domain, owns, systemScope, requires, optional, provides,
|
|
51
|
+
setup, actions, boot, shutdown`.** A `defineProvider` lives in `<Name>Provider.js`.
|
|
49
52
|
|
|
50
53
|
- `setup` builds each provided value with a named `create<Name>` factory from
|
|
51
|
-
a module of its own, never inline.
|
|
54
|
+
a module of its own, never inline. It returns exactly the keys `provides`
|
|
55
|
+
names, or boot refuses it; a value only `actions` needs is built in
|
|
56
|
+
`actions`, which receives the requires.
|
|
52
57
|
- `actions` is a method that returns `create<Name>Actions({ … })`.
|
|
53
58
|
- The hooks take `capabilities`, destructured and typed.
|
|
54
59
|
|
|
@@ -58,15 +63,13 @@ Why: every feature reads the same way, top to bottom.
|
|
|
58
63
|
// good
|
|
59
64
|
const InvoicesFeature = defineFeature({
|
|
60
65
|
id: "feature.invoices",
|
|
61
|
-
|
|
62
|
-
database: "runtime.database"
|
|
63
|
-
},
|
|
66
|
+
owns: [invoiceResource],
|
|
64
67
|
provides: {
|
|
65
68
|
invoices: "feature.invoices"
|
|
66
69
|
},
|
|
67
|
-
/** @param {{
|
|
68
|
-
setup({
|
|
69
|
-
return { invoices: createInvoices({
|
|
70
|
+
/** @param {{ resources: { invoices: import("@jskit-ai/resource-runtime/server/types").ResourceHandle } }} capabilities */
|
|
71
|
+
setup({ resources }) {
|
|
72
|
+
return { invoices: createInvoices({ invoices: resources.invoices }) };
|
|
70
73
|
},
|
|
71
74
|
actions({ invoices }) {
|
|
72
75
|
return createInvoiceActions({ invoices });
|
|
@@ -74,7 +77,7 @@ const InvoicesFeature = defineFeature({
|
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
// bad: the value built inline, the actions an inline array
|
|
77
|
-
setup: ({
|
|
80
|
+
setup: ({ resources }) => ({ invoices: { list: (options) => resources.invoices.queryAll({}, options) } }),
|
|
78
81
|
actions: ({ invoices }) => [{ id: "invoices.list", execute: () => invoices.list() }]
|
|
79
82
|
```
|
|
80
83
|
|
|
@@ -93,7 +96,8 @@ there.**
|
|
|
93
96
|
- Hand-write a contract only for a token with several implementations (such as
|
|
94
97
|
`auth.service`), in its owning package, with optional operations marked
|
|
95
98
|
optional.
|
|
96
|
-
- JSKIT's names: `
|
|
99
|
+
- JSKIT's names: `ResourceHandle`, `ResourceCallOptions`, `SystemContext`
|
|
100
|
+
(`@jskit-ai/resource-runtime/server/types`), `RuntimeKnex`,
|
|
97
101
|
`RuntimeKnexQueryBuilder`, `RuntimeRouter`, `RuntimeLogger`, `RuntimeEnv`,
|
|
98
102
|
`CrudService`, `CrudRepository`.
|
|
99
103
|
|
|
@@ -129,13 +133,13 @@ Why: the type is what turns a call into one step.
|
|
|
129
133
|
// good
|
|
130
134
|
/**
|
|
131
135
|
* The invoices the feature provides.
|
|
132
|
-
* @param {{
|
|
136
|
+
* @param {{ invoices: import("@jskit-ai/resource-runtime/server/types").ResourceHandle }} dependencies
|
|
133
137
|
*/
|
|
134
|
-
function createInvoices({
|
|
138
|
+
function createInvoices({ invoices }) {
|
|
135
139
|
|
|
136
140
|
// bad: untyped, read from a bag in the body
|
|
137
141
|
function createInvoices(dependencies) {
|
|
138
|
-
const
|
|
142
|
+
const invoices = dependencies.invoices;
|
|
139
143
|
```
|
|
140
144
|
|
|
141
145
|
### Required collaborators
|
|
@@ -273,6 +277,14 @@ filled after construction.**
|
|
|
273
277
|
saying so.
|
|
274
278
|
- No late-bound closures between two tokens of one feature: the consumer
|
|
275
279
|
requires the other token.
|
|
280
|
+
- `boot()` and `shutdown()` receive what the feature requires, as `setup()`
|
|
281
|
+
does, and its own setup's values second, as `outputs`:
|
|
282
|
+
`boot(capabilities, { outputs })`.
|
|
283
|
+
- A periodic job is such a capability; JSKIT ships no scheduler. `setup()`
|
|
284
|
+
builds the job, `boot()` starts its timer (`setInterval(runOnce,
|
|
285
|
+
everyMs).unref()`), `shutdown()` clears it and awaits a run in progress, and
|
|
286
|
+
tests call `runOnce()` itself. Every server runs the timer, so a run takes
|
|
287
|
+
its rows with `claim()` ([data access, Layer 4](/guide/framework/data-access#layer-4-locks)).
|
|
276
288
|
|
|
277
289
|
Why: module state is shared by every runtime in the process and outlives
|
|
278
290
|
`shutdown()`.
|
|
@@ -282,8 +294,8 @@ Why: module state is shared by every runtime in the process and outlives
|
|
|
282
294
|
setup() {
|
|
283
295
|
return { jobs: createJobQueue() };
|
|
284
296
|
},
|
|
285
|
-
boot({
|
|
286
|
-
jobs.start();
|
|
297
|
+
boot(_capabilities, { outputs }) {
|
|
298
|
+
outputs.jobs.start();
|
|
287
299
|
},
|
|
288
300
|
|
|
289
301
|
// bad
|
|
@@ -308,14 +320,14 @@ setup({ events }) {
|
|
|
308
320
|
if (!config.remindersEnabled) return {};
|
|
309
321
|
return { reminders: createReminders({ events }) };
|
|
310
322
|
},
|
|
311
|
-
shutdown({
|
|
323
|
+
shutdown(_capabilities, { outputs }) {
|
|
312
324
|
if (!config.remindersEnabled) return;
|
|
313
|
-
return reminders.stop();
|
|
325
|
+
return outputs.reminders.stop();
|
|
314
326
|
}
|
|
315
327
|
|
|
316
328
|
// bad
|
|
317
|
-
shutdown({
|
|
318
|
-
return reminders?.stop?.();
|
|
329
|
+
shutdown(_capabilities, { outputs }) {
|
|
330
|
+
return outputs.reminders?.stop?.();
|
|
319
331
|
}
|
|
320
332
|
```
|
|
321
333
|
|
|
@@ -537,6 +549,44 @@ const actorId = context?.actor?.id || options.context?.user?.id || null;
|
|
|
537
549
|
|
|
538
550
|
## Services and repositories
|
|
539
551
|
|
|
552
|
+
### Resource first
|
|
553
|
+
|
|
554
|
+
**Every table has one table resource, and only its owner feature reads or
|
|
555
|
+
writes it, through the handle it lists in `owns`. A repository holds the
|
|
556
|
+
owner's queries on its handles (a plain CRUD feature's repository is the
|
|
557
|
+
handle itself); a service calls repositories and other features' services or
|
|
558
|
+
capabilities, and never builds a query or reaches another feature's table.** [Data access](/guide/framework/data-access)
|
|
559
|
+
gives the five layers, from one record to reports.
|
|
560
|
+
|
|
561
|
+
Why: ownership, the row policy, validation and audits run only on the
|
|
562
|
+
resource's path; Knex in a repository skips all of them.
|
|
563
|
+
|
|
564
|
+
```js
|
|
565
|
+
// good
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The owner's repository, on its handle.
|
|
569
|
+
* @param {{ receivals: import("@jskit-ai/resource-runtime/server/types").ResourceHandle }} handles
|
|
570
|
+
*/
|
|
571
|
+
function createReceivalsRepository({ receivals }) {
|
|
572
|
+
/**
|
|
573
|
+
* @param {string} id
|
|
574
|
+
* @param {string} arrivedAt
|
|
575
|
+
* @param {import("@jskit-ai/resource-runtime/server/types").ResourceCallOptions} options
|
|
576
|
+
*/
|
|
577
|
+
function markArrived(id, arrivedAt, options) {
|
|
578
|
+
return receivals.patch(id, { status: "arrived", arrivedAt }, options);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return Object.freeze({ markArrived });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// bad
|
|
585
|
+
function markArrived(id, arrivedAt) {
|
|
586
|
+
return knex("receivals").where({ id }).update({ status: "arrived", arrived_at: arrivedAt });
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
540
590
|
### Validated input as is
|
|
541
591
|
|
|
542
592
|
**A method that receives an action's validated input takes it as it is: no
|
|
@@ -564,8 +614,8 @@ async function rename(input = {}, options = {}) {
|
|
|
564
614
|
it, destructure and use.**
|
|
565
615
|
|
|
566
616
|
- An options bag whose fields are optional by contract keeps `options = {}`,
|
|
567
|
-
with no `?.` or second `|| {}` on it. Pass it on as `options`, or
|
|
568
|
-
`
|
|
617
|
+
with no `?.` or second `|| {}` on it. Pass it on as `options`, or as the
|
|
618
|
+
`txOptions` that `withTransaction` hands its work inside a transaction.
|
|
569
619
|
- A parameter or bag no caller passes is dropped and its values written in
|
|
570
620
|
place. A helper's argument takes no default unless a real caller omits it,
|
|
571
621
|
and a default never comes before a required parameter.
|
|
@@ -578,13 +628,13 @@ second method inside the first.
|
|
|
578
628
|
|
|
579
629
|
```js
|
|
580
630
|
// good
|
|
581
|
-
async function list(
|
|
582
|
-
return
|
|
631
|
+
async function list(filters, options = {}) {
|
|
632
|
+
return invoices.queryAll({ filters }, options);
|
|
583
633
|
}
|
|
584
634
|
|
|
585
635
|
// bad
|
|
586
|
-
async function list(
|
|
587
|
-
const
|
|
636
|
+
async function list(filters = {}, options = {}) {
|
|
637
|
+
const context = options?.context || null;
|
|
588
638
|
}
|
|
589
639
|
async function save(invoice, skipAudit = false) {
|
|
590
640
|
```
|
|
@@ -599,8 +649,8 @@ map shows.
|
|
|
599
649
|
|
|
600
650
|
```js
|
|
601
651
|
// good
|
|
602
|
-
function createInvoices({
|
|
603
|
-
async function list(
|
|
652
|
+
function createInvoices({ invoices }) {
|
|
653
|
+
async function list(filters, options) { … }
|
|
604
654
|
async function send(input, options) { … }
|
|
605
655
|
return Object.freeze({
|
|
606
656
|
list,
|
|
@@ -666,180 +716,51 @@ helper, never by hand.**
|
|
|
666
716
|
|
|
667
717
|
| Fact | Reader |
|
|
668
718
|
| --- | --- |
|
|
669
|
-
| the call's transaction, or the client to query through | `resolveTransaction(options)`, `resolveRepoClient(knex, options)` (`@jskit-ai/database-runtime/shared`) |
|
|
670
|
-
| what a service hands its repository | `repositoryCallOptions(options)` (`@jskit-ai/crud-core/server/jsonApiService`), only at the repository call |
|
|
671
719
|
| the signed-in user | `resolveActor(context)`, `resolveActorId(context)` |
|
|
672
720
|
| a tagged result's records, or its document | `simplifyJsonApiResult(result)`, `jsonApiResultValue(result)` (`@jskit-ai/http-runtime/shared`) |
|
|
673
721
|
| a collection's next-page cursor | `jsonApiNextCursor(document)` (`@jskit-ai/http-runtime/shared`) |
|
|
674
722
|
| a created record's id | `resolveCrudJsonApiEntityIdFromResult(result)` (`@jskit-ai/crud-core/server/serviceEvents`) |
|
|
675
723
|
| a CRUD record change's event | `buildCrudRecordChangedEvent({ namespace, operation, entityId, audience })` (`@jskit-ai/crud-core/server/serviceEvents`) |
|
|
676
724
|
| the workspace a request asks for, before any action context | `requestedWorkspaceScope(request)` (`@jskit-ai/workspaces-core/server/support/workspaceRouteInput`) |
|
|
677
|
-
| a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/
|
|
725
|
+
| a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/resource-runtime/server`) |
|
|
678
726
|
|
|
679
|
-
|
|
680
|
-
|
|
727
|
+
A call's transaction is not a fact to read: pass `options` on, and inside a
|
|
728
|
+
transaction the `txOptions` that `withTransaction` hands its work
|
|
729
|
+
([Transactions](/guide/framework/data-access#transactions)).
|
|
730
|
+
|
|
731
|
+
Why: each reader knows every place JSKIT keeps its fact (a tagged result's
|
|
732
|
+
wrapper, the actor in a context), and a hand read misses one.
|
|
681
733
|
|
|
682
734
|
```js
|
|
683
735
|
// good
|
|
684
|
-
const
|
|
736
|
+
const actorId = resolveActorId(context);
|
|
685
737
|
|
|
686
738
|
// bad
|
|
687
|
-
const
|
|
739
|
+
const actorId = context?.actor?.id || context?.auth?.userId || null;
|
|
688
740
|
```
|
|
689
741
|
|
|
690
742
|
### Every row
|
|
691
743
|
|
|
692
|
-
**A server-side
|
|
693
|
-
|
|
694
|
-
(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
- json-rest-api answers a query that names no page with the resource's
|
|
698
|
-
default page, 20 rows, and says nothing about the rest. The CRUD
|
|
699
|
-
repository's `queryDocuments(query)` without a `limit` is the same first
|
|
700
|
-
page, and one with a `limit` above its `queryMaxLimit()` (100) throws,
|
|
701
|
-
because json-rest-api would answer only 100. A CRUD read of every record
|
|
702
|
-
uses the repository's `queryEveryDocument(query, options)`, which answers
|
|
703
|
-
one JSON:API document `{ data, included }` with no `limit`, `cursor` or
|
|
704
|
-
`sort`.
|
|
705
|
-
- The helper reads cursor pages of 1,000 rows (json-rest-api's server-only
|
|
706
|
-
`pageSizeLimit`; a resource whose `queryMaxLimit` is larger uses that) in id
|
|
707
|
-
order until none follows, keeping the call's transaction and context, so no
|
|
708
|
-
row is skipped or repeated. Each page pays the row policy and linkage
|
|
709
|
-
queries once, so 480 rows cost 3 statements, not 15. It sets the page and
|
|
710
|
-
sort itself; sort the rows it returns.
|
|
711
|
-
- A lookup by a unique key (an id, a slug, a token hash, a unique pair such
|
|
712
|
-
as workspace and user) matches at most one row and may read `rows[0]` of a
|
|
713
|
-
plain query. A read of the newest or first few rows names its own `sort`
|
|
714
|
-
and `page`.
|
|
715
|
-
- HTTP list routes keep the caller's page: their default and maximum page
|
|
716
|
-
sizes are public behaviour, not a server-side limit to raise.
|
|
717
|
-
|
|
718
|
-
Why: a list read without a page looks complete until the 21st row, then
|
|
719
|
-
silently drops the rest from every person picker, scope and delivery built on it.
|
|
720
|
-
|
|
721
|
-
```js
|
|
722
|
-
// good
|
|
723
|
-
const rows = await queryAllJsonRestCollectionRows(api.resources.workspaceMemberships, {
|
|
724
|
-
queryParams: { filters: { workspace: workspaceId, status: "active" }, include: ["user"] },
|
|
725
|
-
transaction: resolveTransaction(options),
|
|
726
|
-
format: "plain"
|
|
727
|
-
}, options.context);
|
|
728
|
-
|
|
729
|
-
// bad: the first 20 members only
|
|
730
|
-
const rows = extractJsonRestCollectionRows(await api.resources.workspaceMemberships.query({
|
|
731
|
-
queryParams: { filters: { workspace: workspaceId, status: "active" }, include: ["user"] },
|
|
732
|
-
transaction: resolveTransaction(options),
|
|
733
|
-
format: "plain"
|
|
734
|
-
}, createJsonRestContext(options.context)));
|
|
735
|
-
```
|
|
744
|
+
**A server-side read that needs every row it matches reads them with the
|
|
745
|
+
handle's `queryAll(params, options)`. A `query()` names its page size, and
|
|
746
|
+
answers one page.** [Reads shaped for the screen](/guide/framework/data-access#reads-shaped-for-the-screen)
|
|
747
|
+
gives the rules and examples.
|
|
736
748
|
|
|
737
749
|
### Lists read related rows in batches
|
|
738
750
|
|
|
739
751
|
**A read that builds a list fetches its related rows in batches, an id-list
|
|
740
752
|
query per related table, and groups them in memory, so its query count follows
|
|
741
|
-
the tables and pages it reads, not the rows.**
|
|
742
|
-
|
|
743
|
-
- Collect the ids from the first read, then read each related table with an
|
|
744
|
-
id-list filter: a `searchSchema` entry
|
|
745
|
-
`{ type: "array", actualField: "<column>", filterOperator: "in" }`, read
|
|
746
|
-
through `queryAllJsonRestCollectionRows()`, or the resource's `include`.
|
|
747
|
-
Group the rows by id with `Map.groupBy()`.
|
|
748
|
-
- A count that grows with the rows means something runs per row:
|
|
749
|
-
`getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
|
|
750
|
-
single-record read inside a loop over the list, however the loop is spelled
|
|
751
|
-
(`for`, `map` with `Promise.all`, a helper called per item), or a
|
|
752
|
-
single-record helper reused for a list.
|
|
753
|
-
- Work derived from a row (hashing, preparing a form, assembling a snapshot)
|
|
754
|
-
runs once per distinct input in the call, not once per row it appears in.
|
|
755
|
-
- A JSON:API read also costs a few lookup and visibility queries per page and
|
|
756
|
-
relationship field, and pages hold the resource's `queryMaxLimit` (100 rows
|
|
757
|
-
unless it sets one). That cost follows pages, so three hundred invoices take
|
|
758
|
-
a few more queries than ten, not three hundred more ([Query counts at
|
|
759
|
-
realistic sizes](#query-counts-at-realistic-sizes)).
|
|
760
|
-
|
|
761
|
-
Why: each JSKIT single-record read runs its lookups and row policy too, so a
|
|
762
|
-
read per row repeats all of them.
|
|
763
|
-
|
|
764
|
-
```js
|
|
765
|
-
// good: a read per related table, however many invoices there are
|
|
766
|
-
const invoiceRows = await queryAllJsonRestCollectionRows(api.resources.invoices, {
|
|
767
|
-
queryParams: { filters: { workspace: workspaceId } },
|
|
768
|
-
transaction: resolveTransaction(options),
|
|
769
|
-
format: "plain"
|
|
770
|
-
}, options.context);
|
|
771
|
-
const lineRows = await queryAllJsonRestCollectionRows(api.resources.invoiceLines, {
|
|
772
|
-
queryParams: { filters: { invoiceIds: invoiceRows.map((invoice) => invoice.id) } },
|
|
773
|
-
transaction: resolveTransaction(options),
|
|
774
|
-
format: "plain"
|
|
775
|
-
}, options.context);
|
|
776
|
-
const linesByInvoice = Map.groupBy(lineRows, (line) => line.invoiceId);
|
|
777
|
-
return invoiceRows.map((invoice) => ({ ...invoice, lines: linesByInvoice.get(invoice.id) || [] }));
|
|
778
|
-
|
|
779
|
-
// bad: one more query, and its lookups and row policy, for every invoice
|
|
780
|
-
for (const invoice of invoiceRows) {
|
|
781
|
-
invoice.lines = await invoiceLines.queryDocuments({ invoiceId: invoice.id }, options);
|
|
782
|
-
}
|
|
783
|
-
```
|
|
753
|
+
the tables and pages it reads, not the rows.** [Reads shaped for the
|
|
754
|
+
screen](/guide/framework/data-access#reads-shaped-for-the-screen) shows how.
|
|
784
755
|
|
|
785
756
|
### Bounded access checks
|
|
786
757
|
|
|
787
758
|
**A row policy costs what the rows its query reads cost, never what the
|
|
788
759
|
subject's stored history costs, and a write decides access once instead of
|
|
789
|
-
paying a read policy for every row it writes.**
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
loads everything the subject was ever linked to (every source, notice or
|
|
794
|
-
grant in their history) to build an `IN` list, and never calls another
|
|
795
|
-
owner's access check once per item of that history. The
|
|
796
|
-
[row policies heuristic](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/heuristics/row-policies.md)
|
|
797
|
-
has the policy contract and its package composition.
|
|
798
|
-
- The policy runs on every single-record read, and a CRUD write includes one:
|
|
799
|
-
`createDocument()` and `patchDocumentById()` default to `returning: "full"`,
|
|
800
|
-
which reads the record back through the policy, and `patch` and `delete`
|
|
801
|
-
first look the record up through it.
|
|
802
|
-
- A write path decides access once, inside its transaction. A write that does
|
|
803
|
-
not use its result passes `returning: "minimal"` or `"none"`, and a batch of
|
|
804
|
-
creates (a delivery, an import, a fan-out) goes through
|
|
805
|
-
`createDocuments(payloads, { trx, context })`, which reads nothing back
|
|
806
|
-
unless asked
|
|
807
|
-
([writes that return less](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#writes-that-return-less)).
|
|
808
|
-
Any other batch whose per-row lookup would pay an expensive policy writes
|
|
809
|
-
through a repository method added with `decorateRepository`, on
|
|
810
|
-
`resolveRepoClient(database.knex, options)`, setting the same columns the
|
|
811
|
-
resource would, its ownership columns included.
|
|
812
|
-
- Its test counts the queries of one read and one write for a subject with a
|
|
813
|
-
short history and for one with a long one ([Query counts at realistic
|
|
814
|
-
sizes](#query-counts-at-realistic-sizes)).
|
|
815
|
-
|
|
816
|
-
Why: the row policy runs on every read, write read-back and lookup, so every
|
|
817
|
-
request pays what it costs.
|
|
818
|
-
|
|
819
|
-
```js
|
|
820
|
-
// good: one predicate on the row being read
|
|
821
|
-
function noticeVisibilityPolicy({ query, context, column, value, db }) {
|
|
822
|
-
const userId = resolveActorId(context);
|
|
823
|
-
if (!userId) return false;
|
|
824
|
-
query.where(column("recipientId"), value("recipientId", userId)).whereExists(
|
|
825
|
-
db("sources").select(1)
|
|
826
|
-
.whereColumn("sources.id", column("sourceId"))
|
|
827
|
-
.where("sources.status", "published")
|
|
828
|
-
);
|
|
829
|
-
return true;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// bad: every source the recipient was ever notified about, checked one by one
|
|
833
|
-
async function noticeVisibilityPolicy({ query, context, column, value }) {
|
|
834
|
-
const sourceIds = await noticeHistory.listSourceIds(resolveActorId(context));
|
|
835
|
-
const readable = [];
|
|
836
|
-
for (const sourceId of sourceIds) {
|
|
837
|
-
if (await sources.canRead(sourceId, context)) readable.push(sourceId);
|
|
838
|
-
}
|
|
839
|
-
query.whereIn(column("sourceId"), readable.map((id) => value("sourceId", id)));
|
|
840
|
-
return true;
|
|
841
|
-
}
|
|
842
|
-
```
|
|
760
|
+
paying a read policy for every row it writes.** [Layer
|
|
761
|
+
1](/guide/framework/data-access#layer-1-the-table-resource) has the policy's
|
|
762
|
+
shape and [Layer 3](/guide/framework/data-access#layer-3-many-rows-at-once)
|
|
763
|
+
the batch writes.
|
|
843
764
|
|
|
844
765
|
### Routes
|
|
845
766
|
|
|
@@ -869,7 +790,7 @@ router.register("POST", "/api/invoices/:invoiceId/send", { auth: "required", met
|
|
|
869
790
|
});
|
|
870
791
|
|
|
871
792
|
// bad: the route reads storage itself
|
|
872
|
-
router.register("GET", "/api/invoices", {}, async (request, reply) => reply.send(await
|
|
793
|
+
router.register("GET", "/api/invoices", {}, async (request, reply) => reply.send(await invoices.queryAll({}, { context: request })));
|
|
873
794
|
```
|
|
874
795
|
|
|
875
796
|
## Errors
|
|
@@ -979,7 +900,7 @@ change with its package.
|
|
|
979
900
|
|
|
980
901
|
```js
|
|
981
902
|
// good
|
|
982
|
-
const invoices = createInvoices({
|
|
903
|
+
const invoices = createInvoices({ invoices: resources.invoices, mailer: { send: async () => ({ accepted: true }) } });
|
|
983
904
|
|
|
984
905
|
// bad: production code that exists for the double
|
|
985
906
|
if (typeof mailer.send !== "function") return;
|
|
@@ -1030,72 +951,10 @@ export function resetInvoiceCacheForTests() {
|
|
|
1030
951
|
|
|
1031
952
|
### Query counts at realistic sizes
|
|
1032
953
|
|
|
1033
|
-
**A list, batch or write path's database test
|
|
1034
|
-
`
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
- `countQueries(knex, work)`, from
|
|
1038
|
-
`@jskit-ai/database-runtime/server/testDatabases`, runs `work` and returns
|
|
1039
|
-
its `result`, the `count` of statements the Knex instance or transaction
|
|
1040
|
-
sent meanwhile (a transaction's `BEGIN` and `COMMIT` included) and their
|
|
1041
|
-
SQL in `queries`. Pass the Knex the path runs on: the one the test built its
|
|
1042
|
-
repository or service with.
|
|
1043
|
-
- A route is counted through the server the test boots in its own process
|
|
1044
|
-
(`createServer()`, then `app.inject()`): `countRuntimeQueries(work)`, from
|
|
1045
|
-
the same module, records what the app's own `runtime.database` sends while
|
|
1046
|
-
`work` runs, and leaves out the test's own Knex. A server in another process
|
|
1047
|
-
is out of its reach, so the route's guard boots it in the test.
|
|
1048
|
-
- Pages add a few queries; rows should add none. A path that pages nothing
|
|
1049
|
-
(one read, one write) compares a short and a long history.
|
|
1050
|
-
- When the counts differ more than the pages explain, read `queries`: the
|
|
1051
|
-
statement that repeats once per row is the one to batch. Judge the
|
|
1052
|
-
comparison by what the path has to read; never tune a fixed ceiling until
|
|
1053
|
-
it passes.
|
|
1054
|
-
- When it fails, fix the reads ([Lists read related rows in
|
|
1055
|
-
batches](#lists-read-related-rows-in-batches), [Bounded access
|
|
1056
|
-
checks](#bounded-access-checks)).
|
|
1057
|
-
|
|
1058
|
-
Why: JSON:API reads add queries per page and relationship field, so comparing
|
|
1059
|
-
two sizes separates that cost from a read per row, where one fixed number
|
|
1060
|
-
cannot.
|
|
1061
|
-
|
|
1062
|
-
```js
|
|
1063
|
-
// good
|
|
1064
|
-
import { countQueries, countRuntimeQueries } from "@jskit-ai/database-runtime/server/testDatabases";
|
|
1065
|
-
|
|
1066
|
-
test("the invoice list's queries follow its pages, not its invoices", async () => {
|
|
1067
|
-
const listAt = async (count) => {
|
|
1068
|
-
const workspace = await createWorkspace(knex);
|
|
1069
|
-
await seedInvoices(knex, { workspace, count, linesEach: 5 });
|
|
1070
|
-
return countQueries(knex, () => invoices.listWithLines(workspace, { context }));
|
|
1071
|
-
};
|
|
1072
|
-
const few = await listAt(10);
|
|
1073
|
-
const many = await listAt(300);
|
|
1074
|
-
// Thirty times the invoices may add pages; a read per invoice would add about 290 queries.
|
|
1075
|
-
assert.ok(many.count <= few.count * 2,
|
|
1076
|
-
`${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
// good: the same comparison for the route, through the server this test booted
|
|
1080
|
-
test("GET /api/invoices reads per page, not per invoice", async () => {
|
|
1081
|
-
const app = await createServer({ runtimeEnv });
|
|
1082
|
-
const listAt = async (count) => {
|
|
1083
|
-
const workspace = await createWorkspace(knex);
|
|
1084
|
-
await seedInvoices(knex, { workspace, count, linesEach: 5 });
|
|
1085
|
-
return countRuntimeQueries(() => app.inject({ url: invoicesUrl(workspace), headers: signedIn }));
|
|
1086
|
-
};
|
|
1087
|
-
const few = await listAt(10);
|
|
1088
|
-
const many = await listAt(300);
|
|
1089
|
-
assert.ok(many.count <= few.count * 2,
|
|
1090
|
-
`${few.count} queries for 10 invoices, ${many.count} for 300:\n${many.queries.join("\n")}`);
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
// bad: three invoices, nothing counted, so a read per invoice passes
|
|
1094
|
-
test("the invoice list has its lines", async () => {
|
|
1095
|
-
await seedInvoices(knex, { workspace, count: 3, linesEach: 1 });
|
|
1096
|
-
assert.equal((await invoices.listWithLines(workspace, { context })).length, 3);
|
|
1097
|
-
});
|
|
1098
|
-
```
|
|
954
|
+
**A list, batch or write path's database test proves with
|
|
955
|
+
`expectFlatQueries()` that its statements follow the tables and pages it
|
|
956
|
+
reads, not the rows.** [Tests](/guide/framework/data-access#tests) gives the
|
|
957
|
+
one-line test.
|
|
1099
958
|
|
|
1100
959
|
### Test files run in parallel
|
|
1101
960
|
|
|
@@ -1132,7 +991,11 @@ and Vitest's `maxWorkers` come from `testConcurrency()`.**
|
|
|
1132
991
|
spec file's tests run in order in one worker. With `PLAYWRIGHT_BASE_URL`, the
|
|
1133
992
|
tests use that managed target as its host serves it. Every run is bounded by
|
|
1134
993
|
a `globalTimeout` (20 minutes, or the suite's own measured bound) and ends
|
|
1135
|
-
with its 10 slowest tests.
|
|
994
|
+
with its 10 slowest tests. Each action and navigation is bounded too
|
|
995
|
+
(`actionTimeout` 15 s, `navigationTimeout` 30 s): a stale locator should
|
|
996
|
+
fail in seconds, not minutes, so a test waiting on something that never
|
|
997
|
+
comes fails with that action's own error. An app sets a longer bound only
|
|
998
|
+
for a measured need, never 0. With a `signIn` module, each worker's server is
|
|
1136
999
|
signed in once and every test of that worker starts signed in, instead of
|
|
1137
1000
|
each test signing in. `jskit check` fails a webServer that runs the development server
|
|
1138
1001
|
(`npm run develop`, `vite`), one webServer shared by every worker, and a
|
|
@@ -1309,7 +1172,8 @@ Why: a comment that narrates history is wrong the day after it is written.
|
|
|
1309
1172
|
### Running the checks
|
|
1310
1173
|
|
|
1311
1174
|
`npm run lint` runs ESLint with `@jskit-ai/config-eslint`, and `npm run
|
|
1312
|
-
verify` runs `jskit check` (the package graph, navigation
|
|
1175
|
+
verify` runs `jskit check` (the package graph, navigation, [data
|
|
1176
|
+
access](/guide/framework/data-access#exceptions) and [test
|
|
1313
1177
|
runs](#test-run-rules)), lint, the tests and the build. The foundations'
|
|
1314
1178
|
`eslint.config.mjs` already holds a new
|
|
1315
1179
|
application's code to every check:
|
|
@@ -1350,7 +1214,9 @@ export default [
|
|
|
1350
1214
|
- `formatterConfig(globs)` is the formatter.
|
|
1351
1215
|
- `baseConfig` reports the `jskit/*` rules as warnings on any
|
|
1352
1216
|
`**/src/server/**`, so an existing application sees them before it lists a
|
|
1353
|
-
package; it lists each package as it converts it.
|
|
1217
|
+
package; it lists each package as it converts it. It makes the
|
|
1218
|
+
[data-access rules](/guide/framework/data-access#hard-rules) errors in all
|
|
1219
|
+
code but the client's, tests, migrations and seeds.
|
|
1354
1220
|
- `clientFeaturesConfig(globs)` holds client code to the same rules; see
|
|
1355
1221
|
[client features](/guide/framework/client-features#running-the-checks).
|
|
1356
1222
|
|
|
@@ -1429,6 +1295,7 @@ if (typeof buildSerializer !== "function") {
|
|
|
1429
1295
|
| `filled by <repository>; convert with its supplier` | keeps a setter that another repository's code calls |
|
|
1430
1296
|
| `cross-repository slot <token>` | keeps a probe for a token another repository supplies |
|
|
1431
1297
|
| `shared resource:` | keeps client module state that every instance on the page shares on purpose ([client features](/guide/framework/client-features#module-state)) |
|
|
1298
|
+
| `infrastructure: <what the module is>` | excuses a whole file whose job is data access itself from `jskit/no-direct-knex` or `jskit/no-transaction-reader`, one line per rule before its first line of code ([data access](/guide/framework/data-access#exceptions)) |
|
|
1432
1299
|
|
|
1433
1300
|
One kind of probe gets one prefix and one wording everywhere, and a bare
|
|
1434
1301
|
`typeof x` guard of that kind carries the same comment even where no rule
|
package/guide/agent/index.md
CHANGED
|
@@ -25,6 +25,8 @@ apply or adapt. It does not prescribe an agent host or project orchestrator.
|
|
|
25
25
|
## Framework reference
|
|
26
26
|
|
|
27
27
|
- [Application operations](/guide/framework/application-operations)
|
|
28
|
+
- [Data access](/guide/framework/data-access): read before any code that reads
|
|
29
|
+
or writes a database table
|
|
28
30
|
- [CRUD operations](/guide/framework/crud-operations)
|
|
29
31
|
- [Record form actions](/guide/framework/crud-form-actions)
|
|
30
32
|
- [Typed features](/guide/framework/typed-features): read before writing any
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/agent-docs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.284",
|
|
4
4
|
"description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"skills"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "jskit test test/*.test.js"
|
|
13
|
+
"test": "jskit test test/*.test.js patterns/minimal-foundation/example/tests/server/*.test.js patterns/shell-foundation/example/tests/server/*.test.js"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": "^22.13.0 || ^24.0.0 || ^26.0.0"
|
|
@@ -22,5 +22,8 @@
|
|
|
22
22
|
"jskit",
|
|
23
23
|
"agent",
|
|
24
24
|
"docs"
|
|
25
|
-
]
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@fastify/static": "^10.1.3"
|
|
28
|
+
}
|
|
26
29
|
}
|