@jskit-ai/agent-docs 0.1.131 → 0.1.133

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 (171) hide show
  1. package/guide/agent/app-extras/assistant.md +29 -605
  2. package/guide/agent/app-extras/mobile-capacitor.md +29 -362
  3. package/guide/agent/app-extras/realtime.md +29 -277
  4. package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
  5. package/guide/agent/app-setup/authentication.md +43 -1073
  6. package/guide/agent/app-setup/console.md +26 -298
  7. package/guide/agent/app-setup/database-layer.md +110 -790
  8. package/guide/agent/app-setup/existing-application-migration.md +273 -0
  9. package/guide/agent/app-setup/initial-scaffolding.md +50 -784
  10. package/guide/agent/app-setup/multi-homing.md +39 -712
  11. package/guide/agent/app-setup/quickstart.md +43 -179
  12. package/guide/agent/app-setup/users.md +34 -353
  13. package/guide/agent/index.md +17 -23
  14. package/package.json +2 -2
  15. package/patterns/INDEX.md +7 -7
  16. package/patterns/child-cruds.md +3 -3
  17. package/patterns/client-requests.md +6 -6
  18. package/patterns/crud-authoring.md +94 -0
  19. package/patterns/crud-links.md +1 -1
  20. package/patterns/feature-package/PATTERN.md +108 -0
  21. package/patterns/feature-package/example/booking-engine/package.json +48 -0
  22. package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
  23. package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
  24. package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
  25. package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
  26. package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
  27. package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
  28. package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
  29. package/patterns/filters.md +8 -8
  30. package/patterns/live-actions.md +5 -18
  31. package/patterns/minimal-foundation/PATTERN.md +98 -0
  32. package/patterns/minimal-foundation/example/.nvmrc +1 -0
  33. package/patterns/minimal-foundation/example/AGENTS.md +17 -0
  34. package/patterns/minimal-foundation/example/Procfile +2 -0
  35. package/patterns/minimal-foundation/example/app.json +14 -0
  36. package/patterns/minimal-foundation/example/bin/develop.js +71 -0
  37. package/patterns/minimal-foundation/example/bin/server.js +8 -0
  38. package/patterns/minimal-foundation/example/config/public.js +40 -0
  39. package/patterns/minimal-foundation/example/config/server.js +1 -0
  40. package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
  41. package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
  42. package/patterns/minimal-foundation/example/favicon.svg +7 -0
  43. package/patterns/minimal-foundation/example/gitignore +9 -0
  44. package/patterns/minimal-foundation/example/index.html +13 -0
  45. package/patterns/minimal-foundation/example/jsconfig.json +8 -0
  46. package/patterns/minimal-foundation/example/package.json +57 -0
  47. package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
  48. package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
  49. package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  50. package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
  51. package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
  52. package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
  53. package/patterns/minimal-foundation/example/server.js +195 -0
  54. package/patterns/minimal-foundation/example/src/App.vue +13 -0
  55. package/patterns/minimal-foundation/example/src/main.js +85 -0
  56. package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
  57. package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
  58. package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
  59. package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
  60. package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  61. package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
  62. package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
  63. package/patterns/page-scaffolding.md +20 -17
  64. package/patterns/placements.md +17 -15
  65. package/patterns/row-policies.md +4 -5
  66. package/patterns/server-search.md +3 -3
  67. package/patterns/shell-foundation/PATTERN.md +104 -0
  68. package/patterns/shell-foundation/example/.nvmrc +1 -0
  69. package/patterns/shell-foundation/example/AGENTS.md +17 -0
  70. package/patterns/shell-foundation/example/Procfile +2 -0
  71. package/patterns/shell-foundation/example/app.json +14 -0
  72. package/patterns/shell-foundation/example/bin/develop.js +71 -0
  73. package/patterns/shell-foundation/example/bin/server.js +8 -0
  74. package/patterns/shell-foundation/example/config/public.js +40 -0
  75. package/patterns/shell-foundation/example/config/server.js +1 -0
  76. package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
  77. package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
  78. package/patterns/shell-foundation/example/favicon.svg +7 -0
  79. package/patterns/shell-foundation/example/gitignore +9 -0
  80. package/patterns/shell-foundation/example/index.html +13 -0
  81. package/patterns/shell-foundation/example/jsconfig.json +8 -0
  82. package/patterns/shell-foundation/example/package.json +59 -0
  83. package/patterns/shell-foundation/example/packages/main/package.json +56 -0
  84. package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
  85. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
  86. package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
  87. package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  88. package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
  89. package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
  90. package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
  91. package/patterns/shell-foundation/example/server.js +195 -0
  92. package/patterns/shell-foundation/example/src/App.vue +11 -0
  93. package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
  94. package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
  95. package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  96. package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
  97. package/patterns/shell-foundation/example/src/error.js +19 -0
  98. package/patterns/shell-foundation/example/src/main.js +85 -0
  99. package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
  100. package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
  101. package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
  102. package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
  103. package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
  104. package/patterns/shell-foundation/example/src/placement.js +56 -0
  105. package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
  106. package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
  107. package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
  108. package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
  109. package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  110. package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
  111. package/patterns/shell-foundation/example/vite.config.mjs +81 -0
  112. package/patterns/ui-contract.md +56 -0
  113. package/patterns/ui-testing.md +10 -12
  114. package/reference/autogen/KERNEL_MAP.md +29 -107
  115. package/reference/autogen/PATTERN_INDEX.md +230 -0
  116. package/reference/autogen/README.md +4 -8
  117. package/reference/autogen/packages/agent-docs.md +259 -0
  118. package/reference/autogen/packages/assistant-core.md +3 -3
  119. package/reference/autogen/packages/assistant-runtime.md +32 -17
  120. package/reference/autogen/packages/auth-core.md +31 -33
  121. package/reference/autogen/packages/auth-provider-local-core.md +4 -12
  122. package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
  123. package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
  124. package/reference/autogen/packages/auth-web.md +42 -22
  125. package/reference/autogen/packages/console-core.md +8 -25
  126. package/reference/autogen/packages/console-web.md +5 -5
  127. package/reference/autogen/packages/crud-core.md +61 -17
  128. package/reference/autogen/packages/database-runtime-mysql.md +12 -2
  129. package/reference/autogen/packages/database-runtime-postgres.md +12 -2
  130. package/reference/autogen/packages/database-runtime.md +26 -25
  131. package/reference/autogen/packages/google-rewarded-core.md +19 -104
  132. package/reference/autogen/packages/http-runtime.md +4 -8
  133. package/reference/autogen/packages/http-web.md +32 -0
  134. package/reference/autogen/packages/json-rest-api-core.md +4 -6
  135. package/reference/autogen/packages/kernel.md +109 -390
  136. package/reference/autogen/packages/mobile-capacitor.md +2 -13
  137. package/reference/autogen/packages/realtime.md +29 -26
  138. package/reference/autogen/packages/resource-crud-core.md +6 -0
  139. package/reference/autogen/packages/shell-web.md +69 -54
  140. package/reference/autogen/packages/storage-runtime.md +3 -3
  141. package/reference/autogen/packages/uploads-image-web.md +0 -1
  142. package/reference/autogen/packages/uploads-runtime.md +3 -3
  143. package/reference/autogen/packages/users-core.md +45 -90
  144. package/reference/autogen/packages/users-web.md +5 -7
  145. package/reference/autogen/packages/workspaces-core.md +53 -74
  146. package/reference/autogen/packages/workspaces-web.md +15 -16
  147. package/reference/autogen/tooling/jskit-catalog.md +34 -0
  148. package/reference/autogen/tooling/testUtils.md +4 -4
  149. package/skills/jskit/SKILL.md +38 -31
  150. package/skills/jskit/references/app-operations.md +68 -53
  151. package/skills/jskit/references/crud-operations.md +58 -106
  152. package/skills/jskit/references/material-3.md +1 -1
  153. package/skills/jskit/references/ui-operations.md +41 -44
  154. package/templates/app/AGENTS.md +7 -3
  155. package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
  156. package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
  157. package/guide/agent/generators/advanced-cruds.md +0 -1935
  158. package/guide/agent/generators/crud-generators.md +0 -948
  159. package/guide/agent/generators/intro.md +0 -65
  160. package/guide/agent/generators/row-policies.md +0 -537
  161. package/guide/agent/generators/ui-generators.md +0 -690
  162. package/patterns/crud-scaffolding.md +0 -198
  163. package/patterns/generated-ui-contract-tracking.md +0 -66
  164. package/reference/autogen/packages/assistant.md +0 -68
  165. package/reference/autogen/packages/crud-server-generator.md +0 -215
  166. package/reference/autogen/packages/crud-ui-generator.md +0 -192
  167. package/reference/autogen/packages/feature-server-generator.md +0 -65
  168. package/reference/autogen/packages/ui-generator.md +0 -127
  169. package/reference/autogen/tooling/create-app.md +0 -317
  170. package/reference/autogen/tooling/jskit-cli.md +0 -933
  171. package/reference/autogen/tooling/test-support.md +0 -27
@@ -1,65 +0,0 @@
1
- <!-- Generated by `npm run agent-docs:build` from `packages/agent-docs/site/guide/generators/intro.md`. Do not edit manually. -->
2
-
3
- # Generators
4
-
5
- By the end of the app-setup chapters, the guide app already has the main structural pieces in place:
6
-
7
- - a real shell
8
- - authenticated users
9
- - a database layer
10
- - operator tooling
11
- - workspace-aware routing
12
-
13
- This section changes focus.
14
-
15
- Generators are the part of JSKIT that write app-owned files on top of installed runtime packages.
16
-
17
- That is different from `npx jskit add package ...`:
18
-
19
- - `add package` installs reusable runtime capability into the app
20
- - `generate ...` creates local pages, components, placements, and support files that your app owns directly
21
-
22
- So the normal flow is:
23
-
24
- 1. install the runtime packages that make the feature possible
25
- 2. decide the exact app-owned route, component, or package you want
26
- 3. run `npx jskit generate ...`
27
- 4. review and keep editing the generated files
28
-
29
- One important detail: generator packages such as `ui-generator`, `crud-server-generator`, and `crud-ui-generator` are tooling surfaces. You normally do **not** add them to your app with `jskit add package ...`. You use them through the CLI, and they mutate your app-owned files.
30
-
31
- This intro is also the shared starting point for the rest of the generators section.
32
-
33
- ## Current generator families
34
-
35
- Right now the guide splits generators into two practical groups:
36
-
37
- - `@jskit-ai/ui-generator` for non-CRUD pages, placed UI, subpage hosts, and generic outlets
38
- - `@jskit-ai/crud-server-generator` plus `@jskit-ai/crud-ui-generator` for end-to-end CRUD scaffolding
39
-
40
- For CRUD work, there are also two guide layers on purpose:
41
-
42
- - [CRUD Generators](/guide/generators/crud-generators) teaches the end-to-end workflow for generating a working CRUD
43
- - [Advanced CRUDs](/guide/generators/advanced-cruds) explains the generated package anatomy, ownership structure, and the safest places to customize it
44
- - [Row Policies](/guide/generators/row-policies) explains mandatory SQL visibility before count and pagination, including hierarchy access without package cycles
45
-
46
- The intended reading order is:
47
-
48
- 1. `CRUD Generators`
49
- 2. `Advanced CRUDs`
50
-
51
- ## Why generators matter
52
-
53
- This is one of the main JSKIT ideas:
54
-
55
- - packages own reusable framework/runtime behavior
56
- - generators write the app-local files that make that behavior concrete in your app
57
-
58
- That keeps the runtime reusable without hiding your real app UI behind opaque framework internals.
59
-
60
- ## Next sections
61
-
62
- - [UI Generators](/guide/generators/ui-generators) covers the non-CRUD workflow built around `ui-generator`
63
- - [CRUD Generators](/guide/generators/crud-generators) covers the combined server-and-UI CRUD workflow using `contacts`, nested `addresses`, and in-page `comments`
64
- - [Advanced CRUDs](/guide/generators/advanced-cruds) is the follow-on chapter after `CRUD Generators`, and goes deeper into the generated CRUD structure, ownership boundaries, and search/filter customization
65
- - [Row Policies](/guide/generators/row-policies) is the focused guide for visibility that cannot be expressed as direct user/workspace ownership
@@ -1,537 +0,0 @@
1
- <!-- Generated by `npm run agent-docs:build` from `packages/agent-docs/site/guide/generators/row-policies.md`. Do not edit manually. -->
2
-
3
- # Row Policies
4
-
5
- Generated CRUD ownership filters handle the common cases where every visible row belongs directly to a user, a workspace, or both. Some domains need a different kind of visibility rule:
6
-
7
- - a safety manager can see an assigned organisation unit and every descendant
8
- - membership in another table grants access
9
- - several packages can independently grant access to the same resource
10
- - visibility needs grouped `OR`, `EXISTS`, joins, or a recursive query
11
-
12
- Those rules must be part of the database query before sorting, counting, and pagination. JSKIT supports that through the internal JSON REST host's server-only `rowPolicy` option.
13
-
14
- This chapter covers the normal Knex-backed internal host. `json-rest-api` also supports row policies with AnyAPI storage, but JSKIT does not currently install AnyAPI as the storage engine for `internal.json-rest-api`. The AnyAPI boundary is described separately near the end of this chapter.
15
-
16
- ## The failure row policies prevent
17
-
18
- Do not load a page and filter its records afterward:
19
-
20
- ```js
21
- const document = await organisationUnitsRepository.queryDocuments({
22
- limit: 20,
23
- cursor
24
- }, options);
25
-
26
- document.data = document.data.filter((unit) => canSeeOrganisationUnit(options.context, unit));
27
- ```
28
-
29
- The database has already selected and counted the page. If only two of those twenty rows are visible, the caller receives two rows even when later matching rows exist. The next cursor and total count also describe the unfiltered dataset.
30
-
31
- A row policy changes the SQL query instead:
32
-
33
- ```text
34
- trusted request context
35
-
36
-
37
- client filters + autofilter + mandatory row policy
38
-
39
-
40
- sorting
41
-
42
-
43
- count and pagination
44
-
45
-
46
- JSON:API document
47
- ```
48
-
49
- The response now describes the visible dataset.
50
-
51
- ## Choosing the correct query mechanism
52
-
53
- These mechanisms solve different problems:
54
-
55
- | Requirement | JSKIT mechanism |
56
- | --- | --- |
57
- | The caller chooses a public search or filter value | Normal list query fields, `searchSchema`, and `buildJsonRestQueryParams(...)` |
58
- | Every row has direct `user_id`, `workspace_id`, or both, and writes should receive those values automatically | Generated ownership autofilter |
59
- | Mandatory visibility needs `OR`, `EXISTS`, joins, hierarchy traversal, or grants from another package | A server-only row policy |
60
- | The caller needs permission to create, edit, approve, or delete | Action and route permissions, in addition to row visibility when needed |
61
- | An output field is calculated by SQL | A query projection |
62
-
63
- Autofilter and a row policy can be used on the same resource. Their predicates are combined. Keep autofilter for direct ownership and write stamping; use a row policy only for the visibility that cannot be expressed as fixed owner fields.
64
-
65
- ## The JSKIT host contract
66
-
67
- `@jskit-ai/json-rest-api-core` installs `RowPolicyPlugin` once in the shared internal host. Generated applications and individual CRUD packages must not install the plugin themselves.
68
-
69
- The opt-in point is the existing resource registration in the CRUD provider:
70
-
71
- ```js
72
- import {
73
- INTERNAL_JSON_REST_API,
74
- addResourceIfMissing,
75
- createJsonRestResourceScopeOptions
76
- } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
77
- import { toDatabaseDateTimeUtc } from "@jskit-ai/database-runtime/shared";
78
- import { organisationUnitVisibilityPolicy } from "./organisationUnitVisibilityPolicy.js";
79
- import { resource } from "../shared/organisationUnitResource.js";
80
-
81
- async function registerOrganisationUnitResource(app) {
82
- const api = app.make(INTERNAL_JSON_REST_API);
83
-
84
- await addResourceIfMissing(
85
- api,
86
- "organisationUnits",
87
- createJsonRestResourceScopeOptions(resource, {
88
- rowPolicy: organisationUnitVisibilityPolicy,
89
- writeSerializers: {
90
- "datetime-utc": toDatabaseDateTimeUtc
91
- }
92
- })
93
- );
94
- }
95
- ```
96
-
97
- Keep the policy in a server module. Do not place SQL callbacks in the shared resource module, because the shared module can also be imported by browser code.
98
-
99
- The `rowPolicy` value is the policy function supported by `json-rest-api`. JSKIT passes it through without wrapping it, detecting versions, or retrying without it. Invalid policy definitions fail while the resource is registered.
100
-
101
- Ordinary generated CRUDs remain unchanged. A resource with no `rowPolicy` has no row-policy behavior.
102
-
103
- ## The policy function
104
-
105
- A policy receives the current query and trusted operation context. The most commonly used properties are:
106
-
107
- | Property | Purpose |
108
- | --- | --- |
109
- | `query` | The Knex query being built for the resource. Add the mandatory predicate here. |
110
- | `context` | The context forwarded by the generated route, action, service, and repository. |
111
- | `db` | The active Knex connection or transaction. Use it for subqueries and CTEs. |
112
- | `column(field)` | Resolve and qualify a logical resource field for the active storage query. |
113
- | `value(field, value)` | Convert a logical resource value for storage. |
114
- | `scopeName` | The resource currently being queried. |
115
- | `queryPurpose` | Whether the query is loading a collection, count, include, relationship, or single record. |
116
-
117
- A policy must make an explicit decision:
118
-
119
- - return `true` after adding its visibility predicate
120
- - return `false` to deny every row
121
- - throw when the trusted context itself is invalid and the request must fail
122
-
123
- Do not omit the return value, and do not return the Knex builder. Knex builders are thenable and can execute accidentally when returned from an async function.
124
-
125
- For a direct membership table, a policy can stay small:
126
-
127
- ```js
128
- function organisationUnitMembershipPolicy({ query, context, db, column }) {
129
- const userId = context.visibilityContext?.userId;
130
- const workspaceId = context.visibilityContext?.scopeOwnerId;
131
-
132
- if (!userId || !workspaceId) {
133
- return false;
134
- }
135
-
136
- query.whereExists(function visibleOrganisationUnit() {
137
- this
138
- .select(db.raw("1"))
139
- .from("organisation_unit_memberships as membership")
140
- .where("membership.user_id", userId)
141
- .where("membership.workspace_id", workspaceId)
142
- .whereColumn("membership.organisation_unit_id", column("id"));
143
- });
144
-
145
- return true;
146
- }
147
- ```
148
-
149
- Identity and workspace values come from `visibilityContext`, which JSKIT builds from the authenticated route context. Never take mandatory visibility values from unverified client query parameters.
150
-
151
- ## Group every `OR` grant
152
-
153
- Autofilters, public client filters, and the row policy share one SQL query. A top-level `orWhere(...)` can accidentally escape the predicates that came before it.
154
-
155
- This is unsafe:
156
-
157
- ```js
158
- query.where("workspace_id", workspaceId);
159
- query.orWhereExists(safetyManagerGrant);
160
- ```
161
-
162
- It can mean “correct workspace, or safety grant from any workspace.” Put all policy-owned alternatives inside one group:
163
-
164
- ```js
165
- query.where(function visibilityGrants() {
166
- this.whereExists(directMembershipGrant);
167
- this.orWhereExists(safetyManagerGrant);
168
- });
169
- ```
170
-
171
- The whole group is then combined with the existing autofilter and client predicates using `AND`.
172
-
173
- ## Composing grants without a package cycle
174
-
175
- Suppose the organisation-units package owns the resource, while the safety package grants extra visibility to safety managers.
176
-
177
- Do not make the packages import each other:
178
-
179
- ```text
180
- organisation-units ──imports──▶ safety
181
- ▲ │
182
- └──────── imports ────────┘
183
- ```
184
-
185
- Keep the dependency one-way:
186
-
187
- ```text
188
- safety ──depends on──▶ organisation-units ──depends on──▶ json-rest-api.core
189
- ```
190
-
191
- The organisation-units package owns one application-scoped visibility object. The object has two short operations:
192
-
193
- - `add(...)` during provider registration
194
- - `seal()` once, when the organisation-unit resource boots
195
-
196
- This is domain composition, not a second filtering framework.
197
-
198
- ### The organisation-units visibility object
199
-
200
- ```js
201
- const ORGANISATION_UNIT_VISIBILITY = "organisationUnits.visibility";
202
-
203
- function createOrganisationUnitVisibility() {
204
- const contributions = new Map();
205
- let sealed = false;
206
-
207
- return Object.freeze({
208
- add({ id, apply } = {}) {
209
- const contributionId = String(id || "").trim();
210
- if (sealed) {
211
- throw new Error("Organisation-unit visibility is already sealed.");
212
- }
213
- if (!contributionId || typeof apply !== "function") {
214
- throw new TypeError("Organisation-unit visibility contributions require id and apply.");
215
- }
216
- if (contributions.has(contributionId)) {
217
- throw new Error(`Duplicate organisation-unit visibility contribution: ${contributionId}.`);
218
- }
219
-
220
- contributions.set(contributionId, Object.freeze({
221
- id: contributionId,
222
- apply
223
- }));
224
- },
225
-
226
- seal() {
227
- sealed = true;
228
- return Object.freeze([...contributions.values()]);
229
- }
230
- });
231
- }
232
-
233
- function registerOrganisationUnitVisibility(app, contribution) {
234
- app.make(ORGANISATION_UNIT_VISIBILITY).add(contribution);
235
- }
236
-
237
- export {
238
- ORGANISATION_UNIT_VISIBILITY,
239
- createOrganisationUnitVisibility,
240
- registerOrganisationUnitVisibility
241
- };
242
- ```
243
-
244
- The object belongs to the application container, not module-global state. Separate application instances and tests therefore receive separate contribution sets.
245
-
246
- ### The organisation-units provider
247
-
248
- The owning provider creates the visibility object during `register()` and consumes it during `boot()`:
249
-
250
- ```js
251
- class OrganisationUnitsProvider {
252
- static id = "crud.organisation_units";
253
-
254
- static startsAfter = ["json-rest-api.core", "local.main", "runtime.actions"];
255
-
256
- register(app) {
257
- app.instance(
258
- ORGANISATION_UNIT_VISIBILITY,
259
- createOrganisationUnitVisibility()
260
- );
261
-
262
- registerOrganisationUnitVisibility(app, {
263
- id: "organisation-unit-members",
264
- apply: applyOrganisationUnitMemberVisibility
265
- });
266
-
267
- // Existing repository, service, and action registration stays here.
268
- }
269
-
270
- async boot(app) {
271
- const contributions = app.make(ORGANISATION_UNIT_VISIBILITY).seal();
272
- const rowPolicy = createOrganisationUnitRowPolicy(contributions);
273
- const api = app.make(INTERNAL_JSON_REST_API);
274
-
275
- await addResourceIfMissing(
276
- api,
277
- "organisationUnits",
278
- createJsonRestResourceScopeOptions(resource, {
279
- rowPolicy,
280
- writeSerializers: {
281
- "datetime-utc": toDatabaseDateTimeUtc
282
- }
283
- })
284
- );
285
-
286
- // Existing route registration stays here.
287
- }
288
- }
289
- ```
290
-
291
- JSKIT completes `register()` for every provider before it starts any provider's `boot()`. A dependent package can therefore add its contribution during registration before the organisation-units provider seals the object during boot.
292
-
293
- The owner's contribution represents the access that ordinary organisation-unit users already have. The safety contribution adds another `OR` grant; it does not replace ordinary access. When adopting a row policy for an existing resource, account for every existing visibility path in SQL and prove each one in tests. Existing workspace autofilters still combine with the complete grant group using `AND`.
294
-
295
- ### The composed policy
296
-
297
- Each contribution adds one grouped SQL grant and returns `true`, or returns `false` when it grants nothing for the current context:
298
-
299
- ```js
300
- function createOrganisationUnitRowPolicy(contributions = []) {
301
- return function organisationUnitRowPolicy(policyContext) {
302
- if (contributions.length === 0) {
303
- return false;
304
- }
305
-
306
- policyContext.query.where(function organisationUnitVisibility() {
307
- for (const contribution of contributions) {
308
- this.orWhere(function oneVisibilityGrant() {
309
- const decision = contribution.apply({
310
- ...policyContext,
311
- query: this
312
- });
313
-
314
- if (decision === false) {
315
- this.whereRaw("1 = 0");
316
- return;
317
- }
318
- if (decision !== true) {
319
- throw new Error(
320
- `Organisation-unit visibility contribution ${contribution.id} must return true or false.`
321
- );
322
- }
323
- });
324
- }
325
- });
326
-
327
- return true;
328
- };
329
- }
330
- ```
331
-
332
- Contribution callbacks in this grouping are synchronous. Do not return a promise from them. They build SQL; they do not load records or perform permission checks in JavaScript.
333
-
334
- ### The safety provider
335
-
336
- The safety package depends on organisation-units and registers its grant:
337
-
338
- ```js
339
- class SafetyProvider {
340
- static id = "safety.core";
341
-
342
- register(app) {
343
- registerOrganisationUnitVisibility(app, {
344
- id: "safety-manager-descendants",
345
- apply: applySafetyManagerDescendantVisibility
346
- });
347
-
348
- // Existing safety registrations stay here.
349
- }
350
- }
351
- ```
352
-
353
- The organisation-units package never imports safety. Installing safety adds the grant; omitting safety leaves that grant absent.
354
-
355
- No provider start-order declaration is needed here. JSKIT completes every provider's `register()` phase before any provider begins `boot()`, so safety's contribution is present before organisation-units seals the registry. The normal npm dependency points from safety to organisation-units because safety imports its registration API.
356
-
357
- ## Descendant visibility with a recursive CTE
358
-
359
- A recursive common table expression, usually called a recursive CTE, is a SQL query that starts with one or more rows and repeatedly follows a relationship. It is useful for an adjacency-list tree with `id` and `parent_id`.
360
-
361
- For a safety manager assigned to one unit:
362
-
363
- ```text
364
- assigned unit
365
- ├── child
366
- │ └── grandchild
367
- └── child
368
- ```
369
-
370
- the CTE produces the assigned unit and all descendants. The outer resource query keeps only organisation-unit rows whose id is in that result.
371
-
372
- ```js
373
- function applySafetyManagerDescendantVisibility({
374
- query,
375
- context,
376
- db,
377
- column
378
- }) {
379
- const userId = context.visibilityContext?.userId;
380
- const workspaceId = context.visibilityContext?.scopeOwnerId;
381
-
382
- if (!userId || !workspaceId) {
383
- return false;
384
- }
385
-
386
- const visibleUnitIds = db
387
- .withRecursive("visible_units", ["unit_id"], (cte) => {
388
- cte
389
- .select("assignment.organisation_unit_id as unit_id")
390
- .from("safety_manager_organisation_units as assignment")
391
- .where("assignment.user_id", userId)
392
- .where("assignment.workspace_id", workspaceId)
393
- .unionAll(function descendantUnits() {
394
- this
395
- .select("child.id as unit_id")
396
- .from("organisation_units as child")
397
- .join(
398
- "visible_units as visible",
399
- "child.parent_id",
400
- "visible.unit_id"
401
- )
402
- .where("child.workspace_id", workspaceId);
403
- });
404
- })
405
- .select("unit_id")
406
- .from("visible_units");
407
-
408
- query.whereIn(column("id"), visibleUnitIds);
409
- return true;
410
- }
411
- ```
412
-
413
- Use the real table and column names from the owning packages. The example assumes an acyclic tree and a safety assignment table with `user_id`, `workspace_id`, and `organisation_unit_id`.
414
-
415
- Constrain the workspace in every CTE leg even when the resource also has a workspace autofilter. The CTE is a separate subquery and must not traverse another workspace's hierarchy.
416
-
417
- At minimum, index:
418
-
419
- - `organisation_units.id`
420
- - `organisation_units.parent_id`
421
- - the safety assignment's `user_id`, `workspace_id`, and `organisation_unit_id` access path
422
-
423
- For very large or heavily queried trees, consider a closure table that stores ancestor and descendant pairs. That turns descendant reads into an indexed join or `EXISTS`, but makes writes more involved. Inspect the production query plan before changing the data model.
424
-
425
- ## Missing identity and partial grants
426
-
427
- Mandatory visibility fails closed:
428
-
429
- - a missing user or workspace makes the safety contribution return `false`
430
- - a contribution returning `false` becomes an always-false branch; other contributions can still grant access
431
- - no installed contributions makes the resource policy return `false`, hiding every row
432
- - an omitted or invalid contribution return throws instead of silently removing the policy
433
- - a policy error fails the request; do not catch it and retry without the policy
434
-
435
- This distinction matters when several packages grant access. “This package does not grant access” is not the same as “the entire resource request is invalid.”
436
-
437
- ## Children, includes, and relationship operations
438
-
439
- The target resource's row policy applies when JSON REST loads included records, relationship identifiers, nested includes, and relationship targets used during write validation.
440
-
441
- For a self-referencing organisation-unit relationship, the child target is the same `organisationUnits` resource, so the same row policy applies.
442
-
443
- A separate child resource does not inherit the parent's policy. Give that resource its own `rowPolicy` when its rows have an independent visibility rule.
444
-
445
- Do not add JavaScript filtering after include hydration. It recreates the same count, pagination, and relationship inconsistencies as filtering a top-level collection after the query.
446
-
447
- ## Reads and writes are different contracts
448
-
449
- A row policy controls which existing rows are visible to collection reads and existing-record lookups. Hidden records behave as not found for operations that first load them.
450
-
451
- It does not:
452
-
453
- - stamp ownership values on creates
454
- - replace generated ownership autofilters
455
- - decide whether an action is allowed
456
- - replace service-level domain validation
457
-
458
- Keep create/update/delete permissions in actions and routes. Keep direct workspace/user stamping in autofilter. A caller being able to see a row does not automatically mean the caller may modify it.
459
-
460
- ## Public API and query contracts do not change
461
-
462
- Row policies are mandatory server behavior. They do not add a public list-filter field and do not change:
463
-
464
- - filter definitions
465
- - URLs or query-string serialization
466
- - generated repository method names
467
- - JSON:API document shapes
468
- - active-filter chips
469
- - application call sites
470
-
471
- The generated repository already forwards its trusted context through `createJsonRestContext(...)`. Do not put descendant ids into the URL, accept them from the browser, or materialize the whole visible tree in application memory.
472
-
473
- ## Testing the actual failure
474
-
475
- Use interleaved visible and hidden rows. For example:
476
-
477
- ```text
478
- visible root
479
- hidden root
480
- visible child
481
- hidden child
482
- visible grandchild
483
- ```
484
-
485
- With page size two, assert:
486
-
487
- - page one contains the first two visible rows
488
- - page two contains the remaining visible row
489
- - the total is three
490
- - a cursor from page one reaches only the remaining visible row
491
- - direct access to a hidden record behaves as not found
492
- - missing trusted identity returns no rows or the documented context error
493
- - a self-referencing child load does not expose hidden units
494
- - a separately scoped child resource applies its own policy
495
- - clearing client filters does not remove mandatory visibility
496
-
497
- Also test the package graph and provider lifecycle:
498
-
499
- - safety depends on organisation-units
500
- - organisation-units does not depend on safety
501
- - safety contributes during `register()`
502
- - organisation-units seals contributions during `boot()`
503
- - duplicate contribution ids fail startup
504
- - late contributions fail after the visibility object is sealed
505
-
506
- The `json-rest-api` package owns exhaustive framework tests for pagination, counts, includes, relationship operations, and storage behavior. JSKIT core tests should prove that the plugin is installed and the `rowPolicy` option reaches resource registration. The domain package must own the organisation/safety regression because it owns those tables and grants.
507
-
508
- ## AnyAPI comes later
509
-
510
- `json-rest-api@1.0.26` can apply row policies with `RestApiAnyapiKnexPlugin`, including grouped predicates evaluated after the policy hook returns. JSKIT's current `internal.json-rest-api` host installs `RestApiKnexPlugin`, so the code in this chapter targets normal tables.
511
-
512
- Do not add backend detection, fallback queries, or an `isAnyApi` branch to the normal-table policy in anticipation of a host that does not exist.
513
-
514
- When JSKIT adds an explicit AnyAPI host:
515
-
516
- 1. install `RowPolicyPlugin` in that host after its AnyAPI storage plugin
517
- 2. preserve the same server-only `rowPolicy` resource option
518
- 3. reuse policies that only need logical `column()` and `value()` translation
519
- 4. implement recursive canonical-storage queries explicitly from the AnyAPI descriptor and storage adapter
520
- 5. run the domain regression against the AnyAPI host as a separate supported path
521
-
522
- That is a separate storage implementation. It does not wrap or fall back to the normal-table query.
523
-
524
- ## Review checklist
525
-
526
- - The resource-owning provider passes a server-only `rowPolicy` function.
527
- - Generated applications do not install `RowPolicyPlugin` themselves.
528
- - Ordinary client filters remain client-controlled; mandatory visibility does not.
529
- - Every policy returns `true`, returns `false`, or throws.
530
- - Every policy-owned `OR` is grouped.
531
- - Trusted identity comes from execution context, not query parameters.
532
- - Visibility SQL runs before sorting, count, and pagination.
533
- - The resource owner does not import packages that merely grant extra visibility.
534
- - Contribution state belongs to the application container and is sealed before requests.
535
- - Self-referencing children use the same resource policy; separate child resources declare their own.
536
- - Action permissions and autofilter write stamping remain in place.
537
- - Normal Knex behavior is implemented and tested without speculative AnyAPI branches.