@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.
- package/guide/agent/app-extras/assistant.md +29 -605
- package/guide/agent/app-extras/mobile-capacitor.md +29 -362
- package/guide/agent/app-extras/realtime.md +29 -277
- package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
- package/guide/agent/app-setup/authentication.md +43 -1073
- package/guide/agent/app-setup/console.md +26 -298
- package/guide/agent/app-setup/database-layer.md +110 -790
- package/guide/agent/app-setup/existing-application-migration.md +273 -0
- package/guide/agent/app-setup/initial-scaffolding.md +50 -784
- package/guide/agent/app-setup/multi-homing.md +39 -712
- package/guide/agent/app-setup/quickstart.md +43 -179
- package/guide/agent/app-setup/users.md +34 -353
- package/guide/agent/index.md +17 -23
- package/package.json +2 -2
- package/patterns/INDEX.md +7 -7
- package/patterns/child-cruds.md +3 -3
- package/patterns/client-requests.md +6 -6
- package/patterns/crud-authoring.md +94 -0
- package/patterns/crud-links.md +1 -1
- package/patterns/feature-package/PATTERN.md +108 -0
- package/patterns/feature-package/example/booking-engine/package.json +48 -0
- package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
- package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
- package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
- package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
- package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
- package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
- package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
- package/patterns/filters.md +8 -8
- package/patterns/live-actions.md +5 -18
- package/patterns/minimal-foundation/PATTERN.md +98 -0
- package/patterns/minimal-foundation/example/.nvmrc +1 -0
- package/patterns/minimal-foundation/example/AGENTS.md +17 -0
- package/patterns/minimal-foundation/example/Procfile +2 -0
- package/patterns/minimal-foundation/example/app.json +14 -0
- package/patterns/minimal-foundation/example/bin/develop.js +71 -0
- package/patterns/minimal-foundation/example/bin/server.js +8 -0
- package/patterns/minimal-foundation/example/config/public.js +40 -0
- package/patterns/minimal-foundation/example/config/server.js +1 -0
- package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
- package/patterns/minimal-foundation/example/favicon.svg +7 -0
- package/patterns/minimal-foundation/example/gitignore +9 -0
- package/patterns/minimal-foundation/example/index.html +13 -0
- package/patterns/minimal-foundation/example/jsconfig.json +8 -0
- package/patterns/minimal-foundation/example/package.json +57 -0
- package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
- package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/minimal-foundation/example/server.js +195 -0
- package/patterns/minimal-foundation/example/src/App.vue +13 -0
- package/patterns/minimal-foundation/example/src/main.js +85 -0
- package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
- package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
- package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
- package/patterns/page-scaffolding.md +20 -17
- package/patterns/placements.md +17 -15
- package/patterns/row-policies.md +4 -5
- package/patterns/server-search.md +3 -3
- package/patterns/shell-foundation/PATTERN.md +104 -0
- package/patterns/shell-foundation/example/.nvmrc +1 -0
- package/patterns/shell-foundation/example/AGENTS.md +17 -0
- package/patterns/shell-foundation/example/Procfile +2 -0
- package/patterns/shell-foundation/example/app.json +14 -0
- package/patterns/shell-foundation/example/bin/develop.js +71 -0
- package/patterns/shell-foundation/example/bin/server.js +8 -0
- package/patterns/shell-foundation/example/config/public.js +40 -0
- package/patterns/shell-foundation/example/config/server.js +1 -0
- package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
- package/patterns/shell-foundation/example/favicon.svg +7 -0
- package/patterns/shell-foundation/example/gitignore +9 -0
- package/patterns/shell-foundation/example/index.html +13 -0
- package/patterns/shell-foundation/example/jsconfig.json +8 -0
- package/patterns/shell-foundation/example/package.json +59 -0
- package/patterns/shell-foundation/example/packages/main/package.json +56 -0
- package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
- package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
- package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/shell-foundation/example/server.js +195 -0
- package/patterns/shell-foundation/example/src/App.vue +11 -0
- package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
- package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
- package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/error.js +19 -0
- package/patterns/shell-foundation/example/src/main.js +85 -0
- package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
- package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
- package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
- package/patterns/shell-foundation/example/src/placement.js +56 -0
- package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
- package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
- package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/shell-foundation/example/vite.config.mjs +81 -0
- package/patterns/ui-contract.md +56 -0
- package/patterns/ui-testing.md +10 -12
- package/reference/autogen/KERNEL_MAP.md +29 -107
- package/reference/autogen/PATTERN_INDEX.md +230 -0
- package/reference/autogen/README.md +4 -8
- package/reference/autogen/packages/agent-docs.md +259 -0
- package/reference/autogen/packages/assistant-core.md +3 -3
- package/reference/autogen/packages/assistant-runtime.md +32 -17
- package/reference/autogen/packages/auth-core.md +31 -33
- package/reference/autogen/packages/auth-provider-local-core.md +4 -12
- package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
- package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
- package/reference/autogen/packages/auth-web.md +42 -22
- package/reference/autogen/packages/console-core.md +8 -25
- package/reference/autogen/packages/console-web.md +5 -5
- package/reference/autogen/packages/crud-core.md +61 -17
- package/reference/autogen/packages/database-runtime-mysql.md +12 -2
- package/reference/autogen/packages/database-runtime-postgres.md +12 -2
- package/reference/autogen/packages/database-runtime.md +26 -25
- package/reference/autogen/packages/google-rewarded-core.md +19 -104
- package/reference/autogen/packages/http-runtime.md +4 -8
- package/reference/autogen/packages/http-web.md +32 -0
- package/reference/autogen/packages/json-rest-api-core.md +4 -6
- package/reference/autogen/packages/kernel.md +109 -390
- package/reference/autogen/packages/mobile-capacitor.md +2 -13
- package/reference/autogen/packages/realtime.md +29 -26
- package/reference/autogen/packages/resource-crud-core.md +6 -0
- package/reference/autogen/packages/shell-web.md +69 -54
- package/reference/autogen/packages/storage-runtime.md +3 -3
- package/reference/autogen/packages/uploads-image-web.md +0 -1
- package/reference/autogen/packages/uploads-runtime.md +3 -3
- package/reference/autogen/packages/users-core.md +45 -90
- package/reference/autogen/packages/users-web.md +5 -7
- package/reference/autogen/packages/workspaces-core.md +53 -74
- package/reference/autogen/packages/workspaces-web.md +15 -16
- package/reference/autogen/tooling/jskit-catalog.md +34 -0
- package/reference/autogen/tooling/testUtils.md +4 -4
- package/skills/jskit/SKILL.md +38 -31
- package/skills/jskit/references/app-operations.md +68 -53
- package/skills/jskit/references/crud-operations.md +58 -106
- package/skills/jskit/references/material-3.md +1 -1
- package/skills/jskit/references/ui-operations.md +41 -44
- package/templates/app/AGENTS.md +7 -3
- package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
- package/guide/agent/generators/advanced-cruds.md +0 -1935
- package/guide/agent/generators/crud-generators.md +0 -948
- package/guide/agent/generators/intro.md +0 -65
- package/guide/agent/generators/row-policies.md +0 -537
- package/guide/agent/generators/ui-generators.md +0 -690
- package/patterns/crud-scaffolding.md +0 -198
- package/patterns/generated-ui-contract-tracking.md +0 -66
- package/reference/autogen/packages/assistant.md +0 -68
- package/reference/autogen/packages/crud-server-generator.md +0 -215
- package/reference/autogen/packages/crud-ui-generator.md +0 -192
- package/reference/autogen/packages/feature-server-generator.md +0 -65
- package/reference/autogen/packages/ui-generator.md +0 -127
- package/reference/autogen/tooling/create-app.md +0 -317
- package/reference/autogen/tooling/jskit-cli.md +0 -933
- 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.
|