@jskit-ai/agent-docs 0.1.282 → 0.1.284

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/guide/agent/app-extras/assistant.md +14 -2
  2. package/guide/agent/app-extras/realtime.md +38 -1
  3. package/guide/agent/app-setup/authentication.md +64 -14
  4. package/guide/agent/app-setup/console.md +8 -0
  5. package/guide/agent/app-setup/database-layer.md +23 -103
  6. package/guide/agent/app-setup/existing-application-migration.md +105 -72
  7. package/guide/agent/app-setup/multi-homing.md +94 -32
  8. package/guide/agent/app-setup/quickstart.md +1 -1
  9. package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
  10. package/guide/agent/app-setup/upgrading-jskit.md +8 -3
  11. package/guide/agent/app-setup/users.md +8 -5
  12. package/guide/agent/framework/application-operations.md +1 -1
  13. package/guide/agent/framework/crud-operations.md +63 -45
  14. package/guide/agent/framework/data-access.md +1031 -0
  15. package/guide/agent/framework/sanity-checks.md +26 -27
  16. package/guide/agent/framework/typed-features.md +114 -247
  17. package/guide/agent/index.md +2 -0
  18. package/package.json +6 -3
  19. package/patterns/feature-package/PATTERN.md +25 -17
  20. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  21. package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  22. package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
  23. package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
  24. package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
  25. package/patterns/feature-package/example/variations/types.js +1 -1
  26. package/patterns/minimal-foundation/example/AGENTS.md +4 -0
  27. package/patterns/minimal-foundation/example/package.json +4 -4
  28. package/patterns/minimal-foundation/example/server.js +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +4 -0
  30. package/patterns/shell-foundation/example/package.json +5 -5
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/server.js +4 -1
  33. package/reference/autogen/KERNEL_MAP.md +2 -1
  34. package/reference/autogen/PATTERN_INDEX.md +70 -59
  35. package/reference/autogen/README.md +1 -1
  36. package/reference/autogen/packages/agent-docs.md +7 -3
  37. package/reference/autogen/packages/agent-runtime.md +48 -11
  38. package/reference/autogen/packages/architecture-inspector.md +6 -2
  39. package/reference/autogen/packages/assistant-core.md +4 -4
  40. package/reference/autogen/packages/assistant-runtime.md +33 -13
  41. package/reference/autogen/packages/auth-provider-local-core.md +3 -1
  42. package/reference/autogen/packages/auth-provider-local-db-core.md +65 -14
  43. package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
  44. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
  45. package/reference/autogen/packages/connectors-core.md +22 -2
  46. package/reference/autogen/packages/console-core.md +9 -5
  47. package/reference/autogen/packages/crud-core.md +38 -173
  48. package/reference/autogen/packages/database-runtime.md +8 -32
  49. package/reference/autogen/packages/http-runtime.md +3 -3
  50. package/reference/autogen/packages/http-web.md +12 -0
  51. package/reference/autogen/packages/kernel.md +25 -2
  52. package/reference/autogen/packages/oidc-issuer-core.md +4 -0
  53. package/reference/autogen/packages/payments-core.md +29 -6
  54. package/reference/autogen/packages/realtime.md +15 -6
  55. package/reference/autogen/packages/resource-core.md +30 -5
  56. package/reference/autogen/packages/resource-crud-core.md +30 -34
  57. package/reference/autogen/packages/resource-runtime.md +198 -0
  58. package/reference/autogen/packages/rewarded-core.md +16 -10
  59. package/reference/autogen/packages/shell-web.md +1 -0
  60. package/reference/autogen/packages/users-core.md +26 -14
  61. package/reference/autogen/packages/workspaces-core.md +17 -10
  62. package/reference/autogen/tooling/config-eslint.md +69 -0
  63. package/reference/autogen/tooling/jskit-catalog.md +40 -1
  64. package/reference/autogen/tooling/jskit-release.md +3 -0
  65. package/skills/jskit/SKILL.md +4 -1
  66. package/skills/jskit/references/app-operations.md +1 -1
  67. package/skills/jskit/references/crud-operations.md +62 -44
  68. package/skills/jskit/references/data-access.md +1026 -0
  69. package/skills/jskit/references/database-layer.md +411 -0
  70. package/skills/jskit/references/existing-application-migration.md +105 -72
  71. package/skills/jskit/references/heuristics/INDEX.md +6 -4
  72. package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
  73. package/skills/jskit/references/heuristics/data-access.md +19 -0
  74. package/skills/jskit/references/heuristics/filters.md +6 -6
  75. package/skills/jskit/references/heuristics/row-policies.md +40 -16
  76. package/skills/jskit/references/heuristics/server-search.md +30 -42
  77. package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
  78. package/skills/jskit/references/heuristics/typed-features.md +3 -3
  79. package/skills/jskit/references/multi-homing.md +94 -32
  80. package/skills/jskit/references/pattern-index.md +70 -59
  81. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
  82. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  83. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
  84. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
  88. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
  89. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
  90. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
  91. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  92. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  93. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  94. package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
  95. package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
  96. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
  97. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
  98. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
  99. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
  100. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
  101. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
  102. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
  103. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
  104. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
  105. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
  106. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
  107. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
  108. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
  109. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
  110. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
  111. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
  112. package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +117 -0
  113. package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
  114. package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
  115. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  116. package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
  117. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
  118. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  119. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  120. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
  121. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
  122. package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
  123. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
  124. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +59 -10
  125. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
  126. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
  127. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
  128. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
  131. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
  132. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
  133. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
  134. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
  135. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
  136. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
  137. package/skills/jskit/references/sanity-checks.md +27 -28
  138. package/skills/jskit/references/typed-features.md +114 -247
  139. package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
  140. package/reference/autogen/packages/json-rest-api-core.md +0 -66
  141. package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
  142. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
  143. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
  144. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
  145. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
  146. package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
  147. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
  148. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
  149. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
  150. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
  151. /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 actually costs and does, judge whether that is sane, and
7
- fix the cause when it is not. This page is the JSKIT part of that: the
8
- pitfalls JSKIT's own packages cause, then which JSKIT call, helper or setting
9
- applies each check, with one line on what the check catches and why.
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
- - An unpaged JSON:API query returns the first 20 rows, and a page above the
70
- resource's `queryMaxLimit` (100 unless it sets one) is cut to that size,
71
- both without an error; CRUD `queryDocuments()` refuses such a `limit`. A
72
- read its callers treat as complete uses `queryAllJsonRestCollectionRows()`
73
- or the repository's `queryEveryDocument()`, because the missing rows
74
- otherwise vanish from every picker and delivery built on it
75
- ([Every row](/guide/framework/typed-features#every-row)).
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 `countQueries()` from
82
- `@jskit-ai/database-runtime/server/testDatabases` at a few rows and at a
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
- ([Query counts at realistic
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. That follows pages, so it is expected.
88
- - `getDocumentById()`, `queryDocuments()` or a service `get` inside a loop is
89
- the usual per-row read; batch it with an id-list `searchSchema` filter or
90
- `include`, grouped with `Map.groupBy()` ([Lists read related rows in
91
- batches](/guide/framework/typed-features#lists-read-related-rows-in-batches)).
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: "full"` read
95
- back, so a write that discards its result passes `returning: "minimal"` and
96
- a batch of creates uses `createDocuments()`, in the owner's transaction
97
- ([Bounded access
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, requires, optional, provides, setup, actions, boot,
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
- requires: {
62
- database: "runtime.database"
63
- },
66
+ owns: [invoiceResource],
64
67
  provides: {
65
68
  invoices: "feature.invoices"
66
69
  },
67
- /** @param {{ database: import("@jskit-ai/database-runtime/server/types").RuntimeDatabase }} capabilities */
68
- setup({ database }) {
69
- return { invoices: createInvoices({ knex: database.knex }) };
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: ({ database }) => ({ invoices: { list: () => database.knex("invoices") } }),
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: `RuntimeKnex`, `RuntimeKnexTransaction`,
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 {{ knex: import("@jskit-ai/database-runtime/server/types").RuntimeKnex }} dependencies
136
+ * @param {{ invoices: import("@jskit-ai/resource-runtime/server/types").ResourceHandle }} dependencies
133
137
  */
134
- function createInvoices({ knex }) {
138
+ function createInvoices({ invoices }) {
135
139
 
136
140
  // bad: untyped, read from a bag in the body
137
141
  function createInvoices(dependencies) {
138
- const knex = dependencies.knex;
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({ jobs }) {
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({ reminders }) {
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({ reminders }) {
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
- `{ ...options, trx }` inside a transaction.
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(query, options = {}) {
582
- return resolveRepoClient(knex, options)("invoices").where(query);
631
+ async function list(filters, options = {}) {
632
+ return invoices.queryAll({ filters }, options);
583
633
  }
584
634
 
585
635
  // bad
586
- async function list(query = {}, options = {}) {
587
- const client = options?.trx || knex;
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({ knex }) {
603
- async function list(query) { … }
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/json-rest-api-core/server/jsonRestApiHost`) |
725
+ | a context's workspace id | `resolveWorkspaceScopeValue(context)` (`@jskit-ai/resource-runtime/server`) |
678
726
 
679
- Why: each reader knows every place JSKIT keeps its fact (`trx` or
680
- `transaction`, a tagged result's wrapper), and a hand read misses one.
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 trx = resolveTransaction(options);
736
+ const actorId = resolveActorId(context);
685
737
 
686
738
  // bad
687
- const trx = options?.trx || options?.transaction || null;
739
+ const actorId = context?.actor?.id || context?.auth?.userId || null;
688
740
  ```
689
741
 
690
742
  ### Every row
691
743
 
692
- **A server-side resource query that needs every row it matches reads them
693
- with `queryAllJsonRestCollectionRows(resource, params, context)`
694
- (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`). A query without a
695
- page means "the first 20 rows".**
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
- - A `rowPolicy` adds one predicate to the query it is given: a join, `EXISTS`
792
- or subquery correlated with the row being read, on indexed columns. It never
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 knex("invoices")));
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({ knex, mailer: { send: async () => ({ accepted: true }) } });
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 counts its queries with
1034
- `countQueries()` at a few rows and at a realistic number, and fails when the
1035
- count grows with the rows instead of the pages.**
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. With a `signIn` module, each worker's server is
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 and [test
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
@@ -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.282",
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
  }