@jskit-ai/agent-docs 0.1.283 → 0.1.285

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 (152) 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 +76 -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 +134 -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 +17 -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 +1207 -0
  15. package/guide/agent/framework/sanity-checks.md +26 -27
  16. package/guide/agent/framework/typed-features.md +117 -248
  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 +3 -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 +69 -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 +45 -175
  48. package/reference/autogen/packages/database-runtime-mysql.md +0 -26
  49. package/reference/autogen/packages/database-runtime.md +11 -33
  50. package/reference/autogen/packages/http-runtime.md +3 -3
  51. package/reference/autogen/packages/http-web.md +12 -0
  52. package/reference/autogen/packages/kernel.md +27 -2
  53. package/reference/autogen/packages/oidc-issuer-core.md +4 -0
  54. package/reference/autogen/packages/payments-core.md +28 -6
  55. package/reference/autogen/packages/realtime.md +15 -6
  56. package/reference/autogen/packages/resource-core.md +32 -5
  57. package/reference/autogen/packages/resource-crud-core.md +30 -34
  58. package/reference/autogen/packages/resource-runtime.md +220 -0
  59. package/reference/autogen/packages/rewarded-core.md +16 -10
  60. package/reference/autogen/packages/shell-web.md +1 -0
  61. package/reference/autogen/packages/users-core.md +33 -14
  62. package/reference/autogen/packages/workspaces-core.md +17 -10
  63. package/reference/autogen/tooling/config-eslint.md +74 -0
  64. package/reference/autogen/tooling/jskit-catalog.md +50 -1
  65. package/reference/autogen/tooling/jskit-release.md +5 -0
  66. package/skills/jskit/SKILL.md +4 -1
  67. package/skills/jskit/references/app-operations.md +1 -1
  68. package/skills/jskit/references/crud-operations.md +62 -44
  69. package/skills/jskit/references/data-access.md +1202 -0
  70. package/skills/jskit/references/database-layer.md +411 -0
  71. package/skills/jskit/references/existing-application-migration.md +134 -72
  72. package/skills/jskit/references/heuristics/INDEX.md +6 -4
  73. package/skills/jskit/references/heuristics/crud-authoring.md +26 -15
  74. package/skills/jskit/references/heuristics/data-access.md +19 -0
  75. package/skills/jskit/references/heuristics/filters.md +6 -6
  76. package/skills/jskit/references/heuristics/row-policies.md +40 -16
  77. package/skills/jskit/references/heuristics/server-search.md +30 -42
  78. package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
  79. package/skills/jskit/references/heuristics/typed-features.md +3 -3
  80. package/skills/jskit/references/multi-homing.md +94 -32
  81. package/skills/jskit/references/pattern-index.md +70 -59
  82. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
  83. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  84. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  88. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
  89. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
  90. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
  91. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
  92. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  93. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  94. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  95. package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
  96. package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
  97. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
  98. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
  99. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
  100. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
  101. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
  102. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
  103. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
  104. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
  105. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
  106. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
  107. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
  108. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
  109. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
  110. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
  111. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
  112. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
  113. package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +120 -0
  114. package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
  115. package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
  116. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  117. package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
  118. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
  119. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  120. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  121. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
  122. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
  123. package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
  124. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
  125. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +63 -10
  126. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
  127. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
  128. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
  131. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
  132. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
  133. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
  134. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
  135. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
  136. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
  137. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
  138. package/skills/jskit/references/sanity-checks.md +27 -28
  139. package/skills/jskit/references/typed-features.md +117 -248
  140. package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
  141. package/reference/autogen/packages/json-rest-api-core.md +0 -66
  142. package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
  143. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
  144. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
  145. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
  146. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
  147. package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
  148. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
  149. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
  150. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
  151. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
  152. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
@@ -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)
@@ -110,14 +110,25 @@ The package migrations create and own these tables:
110
110
  | `workspace_invites` | `workspace_id`, `email`, `role_sid`, `status`, `token_hash`, `invited_by_user_id`, `expires_at`, `accepted_at`, `revoked_at` |
111
111
 
112
112
  Application records that belong to a workspace use a non-null `workspace_id`
113
- foreign key to `workspaces.id`. Read and change these tables through the
114
- `workspaces.core` repositories and services, not directly.
113
+ foreign key to `workspaces.id`. Read and change these tables through their
114
+ owners' capabilities (`workspaces`, `workspaces.memberships` and
115
+ `workspaces.core`, below), not directly.
115
116
 
116
117
  ## Server composition
117
118
 
118
119
  Use `workspaces/workspace-server` for role vocabulary, creation policy,
119
120
  invitations, settings, and app-owned invitation email rendering.
120
121
 
122
+ Two server settings are required, and the server refuses to start without
123
+ them: whether new workspaces accept invitations, and how long an invitation
124
+ lasts.
125
+
126
+ ```js
127
+ // config/server.js
128
+ config.workspaceSettings = { defaults: { invitesEnabled: true } };
129
+ config.workspaceMembers = { defaults: { inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000 } };
130
+ ```
131
+
121
132
  Workspace scope must reach repository queries. Parsing a workspace slug or
122
133
  hiding an admin link is not authorization. Membership and role checks happen
123
134
  before an operation discloses records or record existence.
@@ -125,14 +136,12 @@ before an operation discloses records or record existence.
125
136
  ### Workspace-scoped features and routes
126
137
 
127
138
  For a standard CRUD resource owned by a workspace, give its table the
128
- `workspace_id` column, set the resource's `autofilter: "workspace"`, and bind
129
- `defineCrudJsonApiFeature()` with `ownershipFilter: "workspace"` and `scope:
130
- WORKSPACE_CRUD_SCOPE` (from `@jskit-ai/workspaces-core/server/support/workspaceCrud`)
131
- on a workspace surface. JSKIT then filters `workspaceId` by the workspace in the
132
- route. A repository method you add reads the workspace id with
133
- `resolveWorkspaceScopeValue(options.context)`
134
- (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`), which throws when the
135
- context names two workspaces.
139
+ `workspace_id` column, set its table resource's `ownership: "workspace"`, and bind
140
+ `defineCrudFeature()` with `scope: WORKSPACE_CRUD_SCOPE` (from
141
+ `@jskit-ai/workspaces-core/server/support/workspaceCrud`) on a workspace
142
+ surface. JSKIT then filters `workspaceId` by the workspace in the route. A
143
+ repository method you add passes the call's `options` on to the table's handle,
144
+ which scopes every read and write to the context's workspace.
136
145
  `workspace_user` also filters by the signed-in user. See the CRUD operations
137
146
  guide.
138
147
 
@@ -175,9 +184,29 @@ capability provides:
175
184
  | `workspaces.services.members` | Members, roles and invitations |
176
185
  | `workspaces.services.pendingInvitations` | Invitation resolution, redemption and acceptance participants |
177
186
  | `workspaces.services.settings` | Workspace settings |
178
- | `workspaces.repositories.workspaces`, `.workspaceMemberships`, `.workspaceSettings`, `.workspaceInvites` | Repository access inside the application's own transactions |
187
+ | `workspaces.repositories.workspaceSettings`, `.workspaceInvites` | Settings and invitation records inside the application's own transactions |
179
188
  | `workspaces.tenancyProfile` | The resolved tenancy mode and workspace policy |
180
189
 
190
+ The workspaces and their memberships are their own capabilities, for code that
191
+ reads or changes them inside its own transactions, typed `Workspaces` and
192
+ `WorkspacesMemberships` in `@jskit-ai/workspaces-core/server/types`. Every
193
+ method takes the call's `options` last and joins the transaction of the
194
+ `txOptions` it is given.
195
+
196
+ | Capability | Methods |
197
+ | --- | --- |
198
+ | `workspaces` | `findById(id, options)`, `findBySlug(slug, options)` (a deleted workspace is not found, though its slug stays taken), `listSlugsIn`, `findPersonalByOwnerUserId`, `listForUserId(userId, options)`, `insert`, `updateById`, `lock(workspaceId, options)` |
199
+ | `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds` |
200
+
201
+ `addMember` makes the user an active member with the role (`member` when none
202
+ is given), adding the membership or reactivating it. `activeMemberIds` answers
203
+ the active members' user ids at the same cost however many members there
204
+ are, without including their profiles. `lock` takes
205
+ the workspace's row lock until the caller's transaction ends, so work that must
206
+ not interleave for one workspace takes turns; it runs only inside a
207
+ transaction, given its `txOptions`, and answers `{ id }`, or `null` when there
208
+ is no such workspace.
209
+
181
210
  To check one person's membership, for example a notification recipient, call
182
211
  `workspaces.services.members.getMember(workspace, userId, options)`. It answers
183
212
  that member's `listMembers` entry, or `null` for a removed or unknown member,
@@ -225,8 +254,8 @@ workspace routes, `workspaces-web` switches Vuetify to the `workspace-light` or
225
254
  colours. Other routes use the application's themes.
226
255
 
227
256
  New workspaces start from JSKIT's blue palette unless `config/server.js` sets
228
- the application's own. Missing fields keep JSKIT's values; an invalid colour
229
- stops the server at startup:
257
+ the application's own beside the required `invitesEnabled`. Missing colours
258
+ keep JSKIT's values; an invalid colour stops the server at startup:
230
259
 
231
260
  ```js
232
261
  // config/server.js
@@ -301,11 +330,11 @@ authorization and role checks. Do not pass arbitrary HTTP input as options.
301
330
 
302
331
  | Capability method | Contract |
303
332
  | --- | --- |
304
- | `workspaces.repositories.workspaceInvites.withTransaction(work)` | Delegates to the same JSON REST API's managed `transaction(work)`; awaits commit and completion hooks before resolving. Runs `work` again when the database aborts it as a deadlock victim (see [Database layer](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#deadlock-retries)). |
305
- | `workspaces.repositories.workspaceInvites.lockWorkspaceForInvitations(workspaceId, { trx })` | Locks the workspace before application receipt/draft rows when composing preparation. Requires an active transaction. |
306
- | `workspaces.services.members.prepareInvite(workspace, actor, payload, options)` | Requires a managed `options.trx`. Validates the invitation role, replaces pending invitations for that workspace/email, and returns `{ createdInviteId, inviteTokenPreview }`. Never sends. |
307
- | `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` | Requires no `options.trx`. Reloads the committed pending invitation and workspace display data, validates expiry and token correspondence, and sends that invitation without replacement. |
308
- | `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it. Rejects `options.trx`. |
333
+ | `workspaces.repositories.workspaceInvites.withTransaction(options, (txOptions) => …)` | The invitations handle's `withTransaction`: joins the caller's transaction or opens one and hands `work` the `txOptions` to pass on; awaits commit and completion hooks before resolving. Runs `work` again when the database aborts it as a deadlock victim (see [data access](https://mobily-enterprises.github.io/jskit-ai/guide/framework/data-access#transactions)). `{ ...options, outermost: true }` refuses a call that already runs inside one. |
334
+ | `workspaces.lock(workspaceId, txOptions)` (capability `workspaces`) | Locks the workspace before application receipt/draft rows when composing preparation. Requires an active transaction. |
335
+ | `workspaces.services.members.prepareInvite(workspace, actor, payload, txOptions)` | Runs only inside a transaction, given its `txOptions`. Validates the invitation role, replaces pending invitations for that workspace/email, and returns `{ createdInviteId, inviteTokenPreview }`. Never sends. |
336
+ | `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` | Refuses to run inside a transaction: it delivers after the preparation's transaction commits. Reloads the committed pending invitation and workspace display data, validates expiry and token correspondence, and sends that invitation without replacement. |
337
+ | `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it. Refuses to run inside a transaction. |
309
338
  | `workspaces.invite-acceptance-participant` (optional capability) | One application function receiving `({ invite, user }, options)` inside the acceptance transaction. Without it, acceptance activates the membership only. |
310
339
 
311
340
  `sendInvite` accepts `{ createdInviteId, inviteTokenPreview }` from preparation or
@@ -344,21 +373,20 @@ setup: ({ reviewedPeople, people, scopes, audit }) => ({
344
373
  })
345
374
 
346
375
  // Inside an authorized application command, in a feature that requires
347
- // workspaces: "workspaces.core":
376
+ // workspaces: "workspaces.core" and workspaceRecords: "workspaces":
348
377
  const { members } = workspaces.services;
349
378
  const invites = workspaces.repositories.workspaceInvites;
350
- const prepared = await invites.withTransaction(async (trx) => {
351
- const options = { trx, context };
379
+ const prepared = await invites.withTransaction({ context }, async (txOptions) => {
352
380
  // Match acceptance's order: workspace, then application draft/receipt rows.
353
- await invites.lockWorkspaceForInvitations(workspace.id, options);
354
- const reviewed = await reviewedPeople.lockAuthorizedDraft(draftId, workspace.id, actor, options);
355
- const receipt = await reviewedPeople.findMatchingReceipt(reviewed, requestId, options);
381
+ await workspaceRecords.lock(workspace.id, txOptions);
382
+ const reviewed = await reviewedPeople.lockAuthorizedDraft(draftId, workspace.id, actor, txOptions);
383
+ const receipt = await reviewedPeople.findMatchingReceipt(reviewed, requestId, txOptions);
356
384
  if (receipt) return { createdInviteId: receipt.inviteId };
357
385
  const created = await members.prepareInvite(workspace, actor, {
358
386
  email: reviewed.email,
359
387
  roleSid: reviewed.roleSid
360
- }, options);
361
- await reviewedPeople.associateInvitationAndSaveReceipt(reviewed, requestId, created.createdInviteId, options);
388
+ }, txOptions);
389
+ await reviewedPeople.associateInvitationAndSaveReceipt(reviewed, requestId, created.createdInviteId, txOptions);
362
390
  return { createdInviteId: created.createdInviteId };
363
391
  });
364
392
 
@@ -401,8 +429,8 @@ exactly once. Revocation after sending starts cannot recall a message.
401
429
  Register one callback during feature setup to call the required application
402
430
  services. Non-functions throw `TypeError`; duplicate registration throws, and
403
431
  registration closes when acceptance/refusal first begins. The callback's invite,
404
- user and options are shallow frozen. JSKIT owns the transaction and passes its
405
- `trx` and request `context` to the callback.
432
+ user and options are shallow frozen. JSKIT owns the transaction and passes the
433
+ callback the `txOptions` that carry it and the request's `context`.
406
434
 
407
435
  Acceptance checks authentication, recipient email, pending state and expiry under
408
436
  workspace/invitation locks. Before calling the participant, it activates membership
@@ -420,8 +448,8 @@ callback must not commit, roll back, create transactions/savepoints or perform
420
448
  external effects. Participant and later invitation-status failures roll back
421
449
  membership (including its previous role/status), person/training links, scopes,
422
450
  audit and accepted status. Invalid invitations and refusals do not call the
423
- participant. Acceptance/refusal reject borrowed `options.trx`; revocation can
424
- join a managed transaction or create one.
451
+ participant. Acceptance and refusal refuse to run inside the caller's
452
+ transaction; revocation can join one or open its own.
425
453
 
426
454
  Acceptance/refusal return 401 for unauthenticated access, 403 for a recipient
427
455
  email mismatch and 404 for a missing or handled invitation. A matching recipient's
@@ -435,7 +463,7 @@ Preserve `error.transactionOutcome` and `error.cause`: `rolledBack` confirms
435
463
  rollback, `committed` can accompany a failed completion hook, and `unknown`
436
464
  requires resolving the outcome before retrying. Errors after commit cannot undo
437
465
  persistence. See
438
- [managed transaction guidance](existing-application-migration.md#json-rest-v2-integration).
466
+ [managed transaction guidance](existing-application-migration.md#data-access-on-table-resources-json-rest-api-3).
439
467
 
440
468
  Invitation mutations lock workspace before invitation rows. A losing acceptance,
441
469
  refusal, revocation or replacement cannot overwrite accepted/revoked state.
@@ -494,7 +522,41 @@ Workspace surfaces put the slug in their pages root, such as
494
522
  `w/[workspaceSlug]/admin` for administration. Keep the default surface
495
523
  (`surfaceDefaultId`, usually `home`) free of a workspace requirement: `/`,
496
524
  invitation links and other unscoped pages belong to it. With
497
- `webRootAllowed: "no"`, `/` opens that surface's root.
525
+ `webRootAllowed: "no"`, `/` opens that surface's root. The foundation
526
+ defines `home` and its `public` access policy, and the `auth/auth-surface`
527
+ pattern the `auth` surface; the workspace surfaces add a policy and two
528
+ definitions:
529
+
530
+ ```js
531
+ // config/surfaceAccessPolicies.js
532
+ surfaceAccessPolicies.workspace_member = { requireAuth: true, requireWorkspaceMembership: true };
533
+
534
+ // config/public.js
535
+ config.surfaceDefinitions.app = {
536
+ id: "app",
537
+ label: "App",
538
+ pagesRoot: "w/[workspaceSlug]",
539
+ requiresAuth: true,
540
+ requiresWorkspace: true,
541
+ accessPolicyId: "workspace_member"
542
+ };
543
+ config.surfaceDefinitions.admin = {
544
+ id: "admin",
545
+ label: "Admin",
546
+ pagesRoot: "w/[workspaceSlug]/admin",
547
+ requiresAuth: true,
548
+ requiresWorkspace: true,
549
+ accessPolicyId: "workspace_member"
550
+ };
551
+ ```
552
+
553
+ An access policy takes `requireAuth`, `requireWorkspaceMembership`,
554
+ `requireAnyPermissions` and `requireAllPermissions` (permission lists), and
555
+ `requireFlagsAny` and `requireFlagsAll` (flags the server's bootstrap sets in
556
+ `surfaceAccess`). Without `requireAuth` or `requireWorkspaceMembership` it
557
+ takes the surface's `requiresAuth` or `requiresWorkspace`. The browser applies
558
+ the whole policy; the server reads only `requireWorkspaceMembership: false`
559
+ from it, and a route's own `auth` and `permission` authorize it there.
498
560
 
499
561
  `workspaces-web` remembers the last workspace each person opened in this
500
562
  browser. The **Your workspaces** page, `WorkspacesClientElement` from