@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
@@ -22,7 +22,7 @@ How to use it:
22
22
  - [page-scaffolding.md](./page-scaffolding.md)
23
23
  - child crud, nested crud, embedded list, subroute, separate page, parent/child layout
24
24
  - [child-cruds.md](./child-cruds.md)
25
- - crud, crud server, crud ui, resource contract, table creation, migrations, ownership, JSON API, standard screens
25
+ - crud, crud server, crud ui, table resource, CRUD contract, table creation, migrations, ownership, JSON API, standard screens
26
26
  - [crud-authoring.md](./crud-authoring.md)
27
27
  - CRUD links, record placeholders, `paths.page()`, `resolveViewUrl`, `resolveEditUrl`, `resolveParams`
28
28
  - [crud-links.md](./crud-links.md)
@@ -40,11 +40,13 @@ How to use it:
40
40
  - [filters.md](./filters.md)
41
41
  - `searchSchema`, `search: true`, `applyFilter`, server search, query validators, backend filters, internal JSON REST filters
42
42
  - [server-search.md](./server-search.md)
43
- - `actualField`, `storage.writeSerializer`, `storage.virtual`, computed field, virtual field, projection, `createCrudResourceRuntime`, datetime write serialization, temporalPrecision, json-rest-schema dates
44
- - [crud-repository-mapping.md](./crud-repository-mapping.md)
43
+ - `actualField`, `storage.column`, `storage.writeSerializer`, `storage.virtual`, `queryFields`, computed field, virtual field, projection, `defaultTo`, datetime write serialization, temporalPrecision, json-rest-schema dates
44
+ - [table-resource-fields.md](./table-resource-fields.md)
45
+ - data access, database table, knex, raw, SQL, repository, bulk, batch, many rows, set write, upsert, lock, claim, totals, report, aggregate, newest per group, transaction, `trx`, background job, system scope, `systemContext`, owner, owns, owner API, another feature's table, scope test, `expectFlatQueries`, `requireTestDatabase`, `jskit/no-direct-knex`, `jskit/no-transaction-reader`, `jskit/no-hand-scope`, `jskit/sql-literal`, `jskit/knex-reason`, `infrastructure:`
46
+ - [data-access.md](./data-access.md)
45
47
  - row policy, permission filtering, visibility before pagination, recursive CTE, descendants, package cycle, `RowPolicyPlugin`, row policy cost, slow writes through a policy, access check per history item
46
48
  - [row-policies.md](./row-policies.md)
47
- - typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `api.resources.x.query`, every row, list stops at 20 rows, `queryAllJsonRestCollectionRows`, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size, read per row, N+1 queries, slow endpoint, query budget, realistic row counts, slow tests, raised timeout, `migrate.latest()` in tests, test database snapshot, `--test-concurrency=1`, `node --test`, `jskit test`, `testConcurrency()`, `V64_TEST_CONCURRENCY`, Playwright `workers`, Vitest `maxWorkers`, tests one file at a time, test files colliding in parallel, `jskit-test: alone`, `test/*` findings of `jskit check`
49
+ - typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `handle.query`, `queryAll`, every row, list stops at one page, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size, read per row, N+1 queries, slow endpoint, query budget, realistic row counts, slow tests, raised timeout, `migrate.latest()` in tests, test database snapshot, `--test-concurrency=1`, `node --test`, `jskit test`, `testConcurrency()`, `V64_TEST_CONCURRENCY`, Playwright `workers`, Vitest `maxWorkers`, tests one file at a time, test files colliding in parallel, `jskit-test: alone`, `test/*` findings of `jskit check`
48
50
  - [typed-features.md](./typed-features.md)
49
51
  - client features, composable, `use<Name>`, Vue component, client provider, props, emits, `inject`, `provide`, `ref`, `reactive`, `watch`, `addEventListener`, `setInterval`, module state, `Object.freeze` on a composable, `?.()` in a template, `jskit/typed-inject`, `jskit/collaborator-props`, `jskit/fixed-result-shape`, `jskit/released-listeners`, `jskit/no-module-state`, `jskit/no-frozen-vue-state`, `clientFeaturesConfig`, `shared resource:`
50
52
  - [client-features.md](./client-features.md)
@@ -2,7 +2,7 @@
2
2
 
3
3
  # CRUD authoring
4
4
 
5
- Use this file for normal persisted entities, resource contracts, migrations,
5
+ Use this file for normal persisted entities, table resources and CRUD contracts, migrations,
6
6
  JSON API routes, and matching screens.
7
7
 
8
8
  ## Start from product decisions
@@ -13,15 +13,18 @@ choice is missing. Do not turn these decisions into generator options.
13
13
 
14
14
  Read these package-owned source patterns:
15
15
 
16
- - `crud/resource-contract` for the shared field and ownership contract
17
- - `crud/json-api-resource-package` for migration and server composition
16
+ - `database/table-resource` for the table's fields, ownership and lookups
17
+ - `crud/crud-contract` for the HTTP surface on it
18
+ - `crud/crud-feature-package` for migration and server composition
18
19
  - `crud/crud-screen-set` for routed list, view, create, edit, and delete UI
19
20
 
20
21
  ## Normal implementation order
21
22
 
22
23
  1. Author a source-controlled migration in the package that owns the entity.
23
- 2. Define the resource with `defineCrudResource()`.
24
- 3. Bind standard server behavior with `defineCrudJsonApiFeature()`.
24
+ 2. Define the table resource with `defineTableResource()` and its HTTP surface
25
+ with `defineCrudContract()`.
26
+ 3. Bind standard server behavior with `defineCrudFeature()`, which owns the
27
+ table and serves its CRUD contract.
25
28
  4. Add only product-specific service methods, lifecycle, actions, validation,
26
29
  policy, queries, messages, and orchestration.
27
30
  5. Declare the package provider and framework capabilities in `package.json`.
@@ -33,10 +36,10 @@ Read these package-owned source patterns:
33
36
  The migration and resource are both authored contracts. A live schema is useful
34
37
  for understanding an imported system, but it is never the sole source of truth.
35
38
 
36
- Write a field's `operations` only where it differs from the default (output,
37
- create required as the field is, patch; none for `primary: true` or
38
- `hidden: true`; output only for `readOnly: true`), and write a lookup field as `lookup: "<namespace>"`.
39
- The `crud/resource-contract` pattern lists what each expands to.
39
+ A CRUD contract shows and accepts only the fields its `fields` names, so give
40
+ every field the API shows or accepts its operations (a stored `createdAt` and
41
+ `updatedAt` are shown without an entry), and write a lookup field as `lookup: "<namespace>"`.
42
+ The `crud/crud-contract` and `database/table-resource` patterns list what each expands to.
40
43
 
41
44
  ## Ownership and access
42
45
 
@@ -62,21 +65,26 @@ or rewrite a historical baseline.
62
65
 
63
66
  ## Customize without copying CRUD
64
67
 
65
- `defineCrudJsonApiFeature()` deliberately keeps application extension points:
68
+ `defineCrudFeature()` deliberately keeps application extension points:
66
69
 
70
+ - `owns` lists the feature's other tables, and the CRUD table's server-only
71
+ extras (`{ resource, rowPolicy, hooks }`).
67
72
  - `decorateRepository` adds the few persistence operations unique to the
68
- resource.
73
+ resource, on the table's handle.
69
74
  - `decorateService` overrides standard methods or adds domain methods such as
70
75
  `confirm`, `publish`, `cancel`, or `sendReminder`.
71
76
  - `operationLifecycle` wraps a standard operation with `before`, `execute`,
72
77
  `after`, and mutation-only `afterCommit` phases. Mutation phases before commit
73
- receive the same transaction and `execute` receives `standard(nextInput)`.
78
+ receive the same `options` (context and transaction) to pass on, and
79
+ `execute` receives `standard(nextInput)`. A deadlock reruns them, so they
80
+ touch only the database; an email or HTTP call goes in `afterCommit`, which
81
+ runs once.
74
82
  - `actions` exposes non-CRUD service methods through normal validated,
75
83
  permissioned, audited JSKIT actions and optional explicit HTTP routes.
76
84
 
77
85
  A resource whose records carry a version number names it `lockVersionField`
78
- and patches through the repository's `patchDocumentIfLockVersion`; see the
79
- database layer guide's Optimistic locking.
86
+ and patches with the handle's `patch(id, changes, { ...options, lockVersion })`;
87
+ see the data access guide's Layer 4: locks.
80
88
 
81
89
  Keep database reads and writes in repositories. Services and lifecycle hooks
82
90
  orchestrate repositories; they do not issue raw database queries. Put external
@@ -0,0 +1,19 @@
1
+ <!-- Generated from `packages/agent-docs/heuristics/data-access.md`. -->
2
+
3
+ # Data access
4
+
5
+ Use when code reads or writes a database table: a repository, a batch, a lock,
6
+ a total or report, a transaction, a background job, or another feature's table.
7
+
8
+ Pick the layer ([Data access](../data-access.md)):
9
+
10
+ - One record, or one page of records: the owner's handle (`get`, `query`, `create`, `patch`, `delete`).
11
+ - Screens over HTTP: a CRUD contract on the same table resource.
12
+ - Many rows from one action: `createMany`, `patchMany`, `patchWhere`, `deleteWhere`; never a loop of single writes.
13
+ - Two requests could change the same rows: `lockMany` inside `withTransaction`, rows in id order.
14
+ - Work inside `withTransaction`: `txOptions` is only `{ context, trx }`, so a key the work needs, the app's own included, comes from the method's `options`, and a write on the method's own table gets `{ ...options, ...txOptions }` so the caller's `returning`, `missing` or `format` reaches it.
15
+ - A change only while a row still holds a value (open to booked, once): `patch` with `onlyIf`, or `patchWhere` with that value in its filters (a compare-and-set on every database, so of two concurrent writers one wins); a filter on a related table is refused on MySQL and MariaDB (`selection_cross_table`), so filter on the row's own columns, after locking the related rows with `lockMany`, or use `claim`.
16
+ - Counts, sums, buckets: `aggregate`; newest per group: a window include; anything else: one `scopedQuery()` method under `// Knex: <kind> - <reason>`.
17
+ - Every workspace or user at once: a job with `systemScope`, finding its units with an `aggregate` grouped by the ownership field and looping over them, never rows.
18
+ - A table another feature owns: its service or owner API, never its table. A report that joins it joins the `{ query, column }` its owner API answers from `scopedQuery({ alias })`, as a subquery: one statement, both scopes kept.
19
+ - Every path: a scope test and `expectFlatQueries`; a data test fails without `TEST_DB_NAME`.
@@ -65,8 +65,8 @@ Server-side pattern, only when implementing real backend filter semantics:
65
65
  3. Pass the contract's `queryValidator` through the dedicated
66
66
  `listFilterQueryValidator` option of the standard CRUD list validator group
67
67
  at both the route and action boundaries.
68
- 4. Pass the contract's `jsonRestSearchSchema` into `createJsonRestResourceScopeOptions(..., { searchSchema })`.
69
- 5. Call `contract.toJsonRestQuery(query)` before `buildJsonRestQueryParams(...)` in the JSON REST repository path.
68
+ 4. Pass the contract's `jsonRestSearchSchema` as the `searchSchema` of the CRUD table's `owns` entry: `defineCrudFeature({ crud, owns: [{ resource, searchSchema: contract.jsonRestSearchSchema }] })`.
69
+ 5. Override the service's `list` in `decorateService` so it calls `service.list(contract.toJsonRestQuery(query), options)`.
70
70
 
71
71
  Exact file checklist:
72
72
  - create `packages/<crud>/src/shared/<crud>ListFilters.js`
@@ -77,8 +77,8 @@ Exact file checklist:
77
77
  - update `packages/<crud>/src/server/actions.js` so
78
78
  `createStandardCrudListQueryValidators(...)` receives the same
79
79
  `listFilterQueryValidator`
80
- - update the provider's `createJsonRestResourceScopeOptions(...)` call so `searchSchema: listFilterContract.jsonRestSearchSchema` is merged into the internal JSON REST resource
81
- - update `packages/<crud>/src/server/repository.js` so list queries pass `listFilterContract.toJsonRestQuery(query)` into `buildJsonRestQueryParams(...)`
80
+ - update the feature's `owns` entry for the CRUD table so `searchSchema: listFilterContract.jsonRestSearchSchema` reaches the resource
81
+ - update the feature's `decorateService` so `list(query, options)` passes `listFilterContract.toJsonRestQuery(query)` to the standard `list`
82
82
  - update the page-local `listFilters.js` first; only edit `index.vue` if a specialist lookup label/runtime integration is needed
83
83
  - for lookup-backed filters, wire `useCrudListFilterLookups(...)` beside the existing filter runtime instead of replacing `CrudListFilterSurface`
84
84
 
@@ -168,8 +168,8 @@ Good shape:
168
168
  - shared screen runtime passes `filterRuntime.queryParams` into the list request
169
169
  - `packages/receivals/src/shared/receivalListFilters.js`
170
170
  - `createCrudListFilterContract(RECEIVAL_LIST_FILTER_DEFINITIONS, { columns, invalidValues: "reject" })`
171
- - `createJsonRestResourceScopeOptions(resource, { searchSchema: receivalListFilterContract.jsonRestSearchSchema })`
172
- - `buildJsonRestQueryParams(JSON_REST_SCOPE_NAME, receivalListFilterContract.toJsonRestQuery(query))`
171
+ - `owns: [{ resource: receivalResource, searchSchema: receivalListFilterContract.jsonRestSearchSchema }]`
172
+ - `list: (query, options) => service.list(receivalListFilterContract.toJsonRestQuery(query), options)` in `decorateService`
173
173
  - `const listFilters = useCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, { presets: [...] })`
174
174
  - `const filterLookups = useCrudListFilterLookups(RECEIVAL_LIST_FILTER_DEFINITIONS, { values: listFilters.values, ... })`
175
175
  - `queryParams: listFilters.queryParams`
@@ -6,43 +6,67 @@ Use when:
6
6
 
7
7
  - mandatory record visibility needs `OR`, `EXISTS`, joins, memberships, or hierarchy traversal
8
8
  - filtering after pagination produces empty or incomplete pages
9
- - another package grants access to a resource but a reverse dependency would create a cycle
9
+ - another package must narrow who sees a resource's rows but a reverse dependency would create a cycle
10
10
  - the user mentions `RowPolicyPlugin`, recursive CTEs, descendants, or permission-filtered counts
11
11
  - reads or writes of a resource with a policy get slower as records or a person's history grow
12
12
 
13
13
  Read first:
14
14
 
15
- - the owning CRUD provider and shared resource
16
- - the package-owned CRUD server pattern
17
- - `packages/crud-core/test/defineCrudJsonApiFeature.test.js`
15
+ - the owning feature's `owns` entry and the shared table resource
16
+ - the [data access](../data-access.md) guide's Layer 1
17
+ - `packages/resource-runtime/test/resourceRuntime.test.js` (policies, `ownTable`, contributions)
18
18
 
19
19
  Default JSKIT pattern:
20
20
 
21
21
  1. Keep public search and structured filters in the existing list-filter contract.
22
- 2. Keep direct user/workspace ownership and write stamping in autofilter.
22
+ 2. Keep direct user/workspace ownership and write stamping in the table resource's `ownership`.
23
23
  3. Put complex mandatory read visibility in a server-only row policy.
24
- 4. Pass the policy through `createJsonRestResourceScopeOptions(resource, { rowPolicy })` in the owning provider.
25
- 5. Derive identity from trusted execution context.
24
+ 4. Pass the policy in the owner's `owns` entry: `owns: [{ resource, rowPolicy }]`. It reads the owner's other tables through `ownTable(name)`, a read-only subquery, never by table name. Visibility rules live in the row policy, never in hand queries.
25
+ 5. Derive identity from `caller` (`caller.kind`, then `caller.userId`), never from input. In a workspace request a role
26
+ rule reads `context.permissions`, which the server resolves from the membership before any handle runs, with
27
+ `hasPermission(context.permissions, "bookings.view-all")` from `@jskit-ai/kernel/shared/support`: `includes()`
28
+ misses wildcard grants, such as an owner role's `*` or a `bookings.*`.
26
29
  6. Group every policy-owned `OR` branch.
27
30
  7. Return `true` after adding the predicate, return `false` to deny all rows, or throw for invalid trusted context.
28
31
  8. Keep action permissions for writes; visibility is not mutation authorization.
29
32
 
30
33
  Package composition:
31
34
 
32
- - the package that owns the resource also owns the row policy
33
- - a package that grants extra visibility depends on the resource owner
34
- - the resource owner does not import grant packages
35
- - create one application-scoped visibility object during the owner's `register()`
36
- - grant packages add contributions during their `register()`
37
- - the owner seals the contributions and builds the policy during `boot()`
35
+ - the feature that owns the resource also owns the row policy
36
+ - the owner accepts contributions with `{ resource, rowPolicy, acceptsContributions: true }` in `owns`
37
+ - a package that narrows visibility further depends on the resource owner and calls `resources.contributeRowPolicy(name, policy)` on the handles its own `owns` gave it; a contribution is ANDed with the owner's policy inside its own group, so it can only narrow
38
+ - a contribution never grants visibility: wider visibility (shared rows, grants another package records) is a rule in the owner's own policy over its own tables, and a contribution narrows every read of the table, every feature's included, so a rule for one surface is an owner API method instead
39
+ - the resource owner does not import contributing packages
38
40
  - do not use module-global mutable registries
39
41
 
42
+ Example, "your rooms or shared rooms":
43
+
44
+ ```js
45
+ owns: [{
46
+ resource: roomResource,
47
+ acceptsContributions: true,
48
+ rowPolicy({ query, caller, column, ownTable }) {
49
+ if (caller.kind !== "user") return caller.kind !== "anonymous";
50
+ query.where((rooms) => rooms
51
+ .where(column("shared"), true)
52
+ .orWhereIn(column("id"), ownTable("roomMembers").select("room_id").where("user_id", caller.userId)));
53
+ return true;
54
+ }
55
+ }]
56
+ ```
57
+
58
+ A policy is told who the call runs for as `caller`: `user`, `anonymous`,
59
+ `system` or `job`. Background work is `system` or `job`, never a missing user,
60
+ and a job names its person only as `caller.actorId`. A policy hides a row from
61
+ every read, including other tables' links to it, so narrowing what one screen
62
+ lists is a filter on that screen's read.
63
+
40
64
  Cost:
41
65
 
42
66
  - a policy's cost follows the rows its query reads: add a join, `EXISTS` or subquery correlated with the row through `column()` and `value()`, on indexed columns; look at its SQL too, since a statement that runs to kilobytes is planned again on every read that carries it
43
- - never load the subject's whole history (every source, notice or grant they were ever linked to) to build an `IN` list, and never call another owner's access check per item of it; another package's grant is a predicate contribution
44
- - the policy runs on every single-record read, including the read-back after `createDocument()` and `patchDocumentById()` (`returning: "full"`, their default) and the lookup before `patch` and `delete`, so a write pays it once per row it writes
45
- - a write decides access once, inside its transaction; a batch write that would pay an expensive read-back per row follows the typed features guide's [Bounded access checks](../typed-features.md#bounded-access-checks)
67
+ - never load the subject's whole history (every source, notice or grant they were ever linked to) to build an `IN` list, and never call another owner's access check per item of it; another package's restriction is a predicate contribution, and another package's grant is a predicate in the owner's policy over facts the owner keeps
68
+ - the policy runs on every single-record read, including the read-back after a handle's `create()` and `patch()` (`returning: "record"`, their default) and the lookup before `patch` and `delete`, so a write pays it once per row it writes; the many-row writes check access once per statement
69
+ - a write decides access once, inside its transaction; a batch write that would pay an expensive read-back per row follows the data access guide's [Layer 3](../data-access.md#layer-3-many-rows-at-once)
46
70
 
47
71
  Child-resource rules:
48
72
 
@@ -5,28 +5,26 @@
5
5
  Use when:
6
6
  - a CRUD list accepts `q` or other server-side query params
7
7
  - a resource needs alias filters such as `onlyArchived -> archived`
8
- - a list needs access-scoped filters that the repository injects before querying
8
+ - a list needs filters with compound SQL (status buckets, null/not-null semantics)
9
9
  - the user asks where search belongs on the server in a current JSKIT app
10
10
 
11
11
  Core rule:
12
- - for current JSKIT CRUD apps, the server search contract lives in `resource.searchSchema` on the internal JSON REST path
12
+ - the server search contract lives in the table resource's `searchSchema`; a server-only entry (one with `applyFilter`) goes in the owner's `owns` entry, `{ resource, searchSchema }`
13
13
  - route validators decide which public query keys HTTP callers may send
14
- - repositories may add internal-only filter keys before forwarding the query to JSON REST
15
- - structured list filters should use `createCrudListFilterContract(...)` so route/action validators, JSON REST search schema, and repository query normalization are derived from one definition
14
+ - who may see a row is never a filter: it is the row policy ([row-policies.md](./row-policies.md)), applied by the handle on every read
15
+ - structured list filters should use `createCrudListFilterContract(...)` so route/action validators, the search schema, and query normalization are derived from one definition
16
16
 
17
- The standard CRUD repository pattern follows this path:
17
+ The standard CRUD service lists through the table's handle:
18
18
 
19
19
  ```js
20
- return api.resources.contacts.query({
21
- queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, contactListFilterContract.toJsonRestQuery(query)),
22
- format: "jsonapi"
23
- }, createJsonRestContext(context));
20
+ // what defineCrudFeature's service does; a decorateService list maps structured filters first
21
+ return repository.query(buildJsonRestQueryParams("contacts", contactListFilterContract.toJsonRestQuery(query)), { ...options, format: "jsonapi" });
24
22
  ```
25
23
 
26
24
  The normal layer split is:
27
25
  1. `actions.js` / `registerRoutes.js`: accept and validate public query keys
28
- 2. `repository.js`: add internal-only filter keys such as visibility scope, or call `listFilterContract.toJsonRestQuery(query)` for structured filters
29
- 3. provider/resource registration: merge backend filter behavior into JSON REST with `createJsonRestResourceScopeOptions(resource, { searchSchema })`
26
+ 2. the feature's `decorateService`: call `listFilterContract.toJsonRestQuery(query)` for structured filters before the standard `list`; visibility belongs in the row policy, never in a filter key
27
+ 3. the owner feature: backend filter behavior goes in the table resource's `searchSchema`, or a server-only one in its `owns` entry (`{ resource, searchSchema }`)
30
28
 
31
29
  ### Shared list and HTTP transport
32
30
 
@@ -74,13 +72,13 @@ Do not use `search: true` when:
74
72
  - you want a different public key than the storage field
75
73
  - you need one key to search several fields
76
74
  - you need custom SQL semantics
77
- - you need an internal-only injected filter
75
+ - you need a key the service maps from a structured filter
78
76
 
79
77
  Use explicit `searchSchema` when:
80
78
  - the public key differs from the field name
81
79
  - one key searches multiple fields
82
80
  - the route/API contract should stay stable even if field names change
83
- - the filter is internal-only and injected by the repository
81
+ - the key is internal and only the service's structured-filter mapping sends it
84
82
  - you need to override the simple `search: true` behavior
85
83
 
86
84
  Example:
@@ -110,32 +108,23 @@ Important merge rule:
110
108
  Use `applyFilter` when:
111
109
  - `actualField`, `oneOf`, and normal operators are not enough
112
110
  - the filter needs compound SQL
113
- - the filter encodes visibility rules, status buckets, or null/not-null semantics that are more complex than a direct field comparison
111
+ - the filter encodes status buckets or null/not-null semantics that are more complex than a direct field comparison (never visibility: that is the row policy)
114
112
 
115
113
  Example:
116
114
 
117
115
  ```js
118
116
  searchSchema: {
119
- "__viewerAccess": {
117
+ dueState: {
120
118
  type: "string",
121
119
  applyFilter(query, rawValue) {
122
120
  const value = String(rawValue || "").trim();
123
-
124
- query.whereNull("deleted_at");
125
- if (value === "privileged") {
121
+ if (value === "overdue") {
122
+ query.whereNull("paid_at").where("due_at", "<", new Date());
126
123
  return;
127
124
  }
128
-
129
- const userId = value.startsWith("user:")
130
- ? String(value.slice(5) || "").trim()
131
- : "";
132
-
133
- query.where(function () {
134
- this.where("user_added", 0);
135
- if (userId) {
136
- this.orWhere("added_by_user_id", userId);
137
- }
138
- });
125
+ if (value === "paid") {
126
+ query.whereNotNull("paid_at");
127
+ }
139
128
  }
140
129
  }
141
130
  }
@@ -143,6 +132,7 @@ searchSchema: {
143
132
 
144
133
  Keep `applyFilter` focused on query semantics:
145
134
  - build the SQL/knex filter there
135
+ - read only the row's own columns in a filter a `patchWhere` or `deleteWhere` decides on: one that reads another table through a subquery is not detected, and is no compare-and-set on MySQL and MariaDB
146
136
  - do not put permission checks there
147
137
  - do not turn it into a second service layer
148
138
 
@@ -167,7 +157,7 @@ input: composeSchemaDefinitions([
167
157
  That means:
168
158
  - pagination, `q`, parent filters, includes, sparse fieldsets, and the
169
159
  structured filter keys remain one standard group
170
- - the repository may still add internal-only keys later
160
+ - the service may still map structured keys before the standard list
171
161
 
172
162
  Use the same `listFilterQueryValidator` option with
173
163
  `createCrudJsonApiRouteContracts(...)` at the route boundary. The layers
@@ -196,14 +186,17 @@ const receivalListFilterContract = createCrudListFilterContract(
196
186
  );
197
187
  ```
198
188
 
199
- Use `receivalListFilterContract.queryValidator` at route/action boundaries. Use `receivalListFilterContract.jsonRestSearchSchema` when registering the JSON REST resource. Use `receivalListFilterContract.toJsonRestQuery(query)` before `buildJsonRestQueryParams(...)`.
189
+ Use `receivalListFilterContract.queryValidator` at route/action boundaries. Use `receivalListFilterContract.jsonRestSearchSchema` as the `searchSchema` of the table's `owns` entry. Use `receivalListFilterContract.toJsonRestQuery(query)` in the service's `list` before the standard one.
200
190
 
201
191
  That contract preserves arrays for `enumMany` and `recordIdMany`, maps range filters to internal JSON REST search keys, and keeps those internal keys out of the public HTTP query contract.
202
192
 
203
193
  Do not add route validators for:
204
- - repository-injected internal keys such as `"__viewerAccess"`
194
+ - internal keys that only the service's structured-filter mapping sends
205
195
  - filters that must never be supplied by the client directly
206
196
 
197
+ Who may see a row is never a search key at all: it is the row policy, which
198
+ the handle applies to every read whatever the query says.
199
+
207
200
  ## Recommended Shapes
208
201
 
209
202
  Simple direct field exposure:
@@ -236,15 +229,10 @@ searchSchema: {
236
229
  }
237
230
  ```
238
231
 
239
- Repository-injected internal filter:
232
+ Visibility, by the owner, on every read:
240
233
 
241
234
  ```js
242
- function buildScopedQuery(query = {}, context = null) {
243
- return {
244
- ...(query || {}),
245
- "__viewerAccess": resolveViewerAccess(context)
246
- };
247
- }
235
+ owns: [{ resource: contactResource, rowPolicy: ownContactsOrShared }]
248
236
  ```
249
237
 
250
238
  ## Keep These Boundaries Clean
@@ -252,8 +240,8 @@ function buildScopedQuery(query = {}, context = null) {
252
240
  - Put backend filter behavior in `searchSchema`, not in page code.
253
241
  - Use `createCrudListFilterContract(...)` for structured list filters before writing custom JSON REST query glue.
254
242
  - Put permission checks in the action/service layer, not in `applyFilter`.
255
- - Put repository-only scoping in the repository, not in route query validation.
256
- - Do not expose a query key publicly just because the repository uses it internally.
243
+ - Put visibility in the owner's row policy, never in a search key or route query validation.
244
+ - Do not expose a query key publicly just because the service maps to it internally.
257
245
  - Do not rely on route validators alone; a public key still needs a matching backend search definition.
258
246
 
259
247
  ## Quick Rule Of Thumb
@@ -262,4 +250,4 @@ function buildScopedQuery(query = {}, context = null) {
262
250
  - Public alias or multi-field search: `searchSchema`
263
251
  - Complex SQL semantics: `searchSchema` + `applyFilter`
264
252
  - Public HTTP query key: add it through the standard route/action query group
265
- - Internal-only repository key: inject it in `repository.js`, skip the public validator
253
+ - Who may see a row: the owner's row policy, never a key
@@ -0,0 +1,78 @@
1
+ <!-- Generated from `packages/agent-docs/heuristics/table-resource-fields.md`. -->
2
+
3
+ # Table Resource Fields
4
+
5
+ Use when:
6
+ - a table resource field needs an explicit column mapping
7
+ - a field should appear in records but is not a real column
8
+ - the user mentions computed fields, projections or date write serialization
9
+
10
+ Default JSKIT pattern:
11
+ 1. The table resource's schema describes the record; each field maps to its column itself.
12
+ 2. Use `storage: { column: "<column>" }` (or `actualField`) only when the default snake_case column is wrong.
13
+ 3. Standard writable `dateTime` fields are serialized at the write seam automatically; `storage.writeSerializer` is only for non-default serialization.
14
+ 4. A computed output field is `storage: { virtual: true }` in the shared schema, and its SQL goes in the owner's `owns` entry as `queryFields`, on the server.
15
+ 5. Handles apply those projections on every read; no repository method re-selects them.
16
+ 6. A field's default value is `defaultTo`; `default` is refused, since json-rest-api would ignore it.
17
+ 7. A field no request may see (an ownership id, a bearer token) is `hidden: true`, and stays hidden: the owner's code
18
+ reads it by naming it in a handle read's `fields`, and a system context may name it through another feature's
19
+ CRUD repository. Unhiding it puts it in every read's answer, other features' included; never read it with `scopedQuery`.
20
+
21
+ Field metadata rules:
22
+
23
+ ```js
24
+ // a column override
25
+ createdAt: { type: "dateTime", required: true, storage: { column: "created_at" } }
26
+
27
+ // a non-default write serializer
28
+ arrivalDatetime: { type: "dateTime", required: true, storage: { writeSerializer: "datetime-utc" } }
29
+
30
+ // a computed output field
31
+ remainingWeight: { type: "number", required: true, storage: { virtual: true } }
32
+ ```
33
+
34
+ - omit `actualField`/`storage` when the field is column-backed and the default snake_case mapping is correct
35
+ - `storage: { virtual: true }` cannot also define `actualField`, `storage.column` or `storage.writeSerializer`
36
+ - `storage: { virtual: true }` fields are never written
37
+
38
+ Computed fields (server only):
39
+
40
+ ```js
41
+ const ReceivalsFeature = defineFeature({
42
+ id: "receivals",
43
+ owns: [{
44
+ resource: receivalResource,
45
+ queryFields: {
46
+ remainingWeight: {
47
+ type: "number",
48
+ select({ raw, column }) {
49
+ return raw("?? - coalesce(??, 0)", [column("receivedWeight"), column("processedWeight")]);
50
+ }
51
+ }
52
+ }
53
+ }],
54
+ setup: ({ resources }) => ({ receivals: createReceivalsRepository(resources) })
55
+ });
56
+ ```
57
+
58
+ Keep projection SQL in the server feature that owns the table, never in the shared resource that ships to the browser.
59
+ `select` receives `raw`, which builds one SQL expression from a string literal and its bindings (a value only: it never runs on its own),
60
+ `column(field)`, which names a field's column, and the read's `filters`; never Knex. The expression is the field's value
61
+ only, so it reads no other table.
62
+
63
+ What the resource runtime does for you:
64
+ - reads select only column-backed fields plus the registered query fields
65
+ - writes serialize standard writable `dateTime` fields, and any explicit `storage.writeSerializer`
66
+ - database reads serialize `date`, `time` and `dateTime` values to their strict `json-rest-schema` strings before validation, respecting `temporalPrecision`
67
+ - search and filters only use column-backed fields
68
+
69
+ Avoid:
70
+ - `clearSelect()` / re-select hacks in a repository method to add a computed field
71
+ - SQL fragments or joins in shared field metadata
72
+ - `storage` modes beyond the documented `column`, `writeSerializer` and `virtual`
73
+
74
+ Review checks:
75
+ - one table resource per table, and field definitions own their mapping
76
+ - non-default write serialization lives in `storage.writeSerializer`, not in repository code
77
+ - every `storage: { virtual: true }` field has a matching `queryFields` entry in the owner's `owns`
78
+ - no JavaScript `Date` object crosses a strict resource validation boundary; scoped-query reports return ISO/RFC 3339 strings explicitly
@@ -63,11 +63,11 @@ ends with the same link.
63
63
  - [One helper per package](../typed-features.md#one-helper-per-package)
64
64
  - `options?.trx || options?.transaction`, a hand-read actor, cursor or created id
65
65
  - [One reader per fact](../typed-features.md#one-reader-per-fact)
66
- - `api.resources.x.query(…)` with no `page` for a list, `queryDocuments(query)` with no `limit` in a service, a list that stops at 20 rows, `page: { size: 500 }` or `limit: 500` that returns 100 rows
66
+ - a list read with `query()` where every row is meant (`queryAll()`), a list that stops at one page, `page: { size: 500 }` refused above the page limit
67
67
  - [Every row](../typed-features.md#every-row)
68
- - `await getDocumentById(…)`, `queryDocuments(…)` or a service `get` inside a loop or a `Promise.all` over a list, a single-record helper reused for a list, N+1 queries, a list whose query count grows with its rows, a slow list endpoint
68
+ - `await handle.get(…)`, `handle.query(…)` or a service `get` inside a loop or a `Promise.all` over a list, a single-record helper reused for a list, N+1 queries, a list whose query count grows with its rows, a slow list endpoint
69
69
  - [Lists read related rows in batches](../typed-features.md#lists-read-related-rows-in-batches)
70
- - a `rowPolicy` that loads a person's whole history or calls an access check per item, `createDocument` per row of a batch, a write that gets slower as history grows
70
+ - a `rowPolicy` that loads a person's whole history or calls an access check per item, `create()` per row of a batch instead of `createMany()`, a write that gets slower as history grows
71
71
  - [Bounded access checks](../typed-features.md#bounded-access-checks)
72
72
  - a route that queries storage, `boot*.js` holding routes
73
73
  - [Routes](../typed-features.md#routes)