@jskit-ai/agent-docs 0.1.82 → 0.1.83
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-setup/working-with-the-jskit-cli.md +12 -0
- package/guide/agent/generators/advanced-cruds.md +16 -0
- package/guide/agent/generators/intro.md +2 -0
- package/guide/agent/generators/row-policies.md +545 -0
- package/package.json +1 -1
- package/patterns/INDEX.md +3 -0
- package/patterns/row-policies.md +66 -0
- package/reference/autogen/packages/json-rest-api-core.md +1 -1
- package/reference/autogen/packages/kernel.md +5 -0
- package/reference/autogen/packages/users-web.md +33 -0
- package/reference/autogen/tooling/jskit-cli.md +19 -2
|
@@ -100,6 +100,18 @@ npm run jskit:update -- --registry https://registry.example.com
|
|
|
100
100
|
npm run release -- --registry https://registry.example.com
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### Update JSKIT packages across an app workspace
|
|
104
|
+
|
|
105
|
+
`npx jskit app update-packages` owns the complete app-wide JSKIT update. At the app root, it installs the exact latest registry version of every `@jskit-ai/*` package listed in `dependencies`, `devDependencies`, `optionalDependencies`, or `peerDependencies`. Exact root versions keep the installed app and its managed lock state reproducible.
|
|
106
|
+
|
|
107
|
+
When the app declares npm workspaces, the command asks npm for the workspace graph and aligns JSKIT references in each workspace `package.json` and `package.descriptor.mjs` to the latest major range, such as `0.x`. Descriptor dependency mutations are included whether they use a direct string or a conditional `{ version, when }` record. It then runs `npm update --workspaces` for those packages so `package-lock.json` reflects the aligned ranges. Non-JSKIT dependencies and the public descriptor format are left alone.
|
|
108
|
+
|
|
109
|
+
The update reports elapsed progress for registry and install work. It also refreshes JSKIT-managed migrations and CI after root package changes. Preview the complete operation without changing manifests, descriptors, the lockfile, migrations, or CI with:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx jskit app update-packages --dry-run
|
|
113
|
+
```
|
|
114
|
+
|
|
103
115
|
For older apps that still carry copied maintenance scripts, the migration path is:
|
|
104
116
|
|
|
105
117
|
```bash
|
|
@@ -6,6 +6,8 @@ The earlier CRUD chapter shows the workflow. This chapter shows the anatomy.
|
|
|
6
6
|
|
|
7
7
|
If you have not read [CRUD Generators](/guide/generators/crud-generators) yet, start there first. This chapter assumes you already understand the basic generation flow and want to inspect or customize what it produced.
|
|
8
8
|
|
|
9
|
+
When a CRUD's mandatory visibility needs joins, `EXISTS`, grouped grants, or hierarchy traversal instead of direct owner columns, continue with [Row Policies](/guide/generators/row-policies). Do not filter an already-paginated result in `service.js`.
|
|
10
|
+
|
|
9
11
|
Once you generate `contacts`, you do **not** get one magical black-box CRUD object. You get:
|
|
10
12
|
|
|
11
13
|
- an app-local server package under `packages/contacts/`
|
|
@@ -824,6 +826,7 @@ Use this rule of thumb when deciding where to edit:
|
|
|
824
826
|
| Change permissions or channels | `actions.js` | This is the action contract boundary |
|
|
825
827
|
| Change default ordering, searchable fields, or ownership autofilter | `contactResource.js` | The shared resource is the single source of truth for both CRUD contract and internal JSON:API resource config |
|
|
826
828
|
| Change SQL, joins, parent filters, or advanced search | `repository.js` | This is the data-access layer |
|
|
829
|
+
| Add mandatory SQL visibility that must run before count and pagination | server policy module plus the provider's `createJsonRestResourceScopeOptions(..., { rowPolicy })` call | The internal JSON REST host applies the policy to every storage query for that resource |
|
|
827
830
|
| Add cross-record or domain rules on save/delete | `service.js` | This is business logic |
|
|
828
831
|
| Change shared CRUD screen chrome, load states, or retry behavior | `users-web` shared screen components | Generated pages consume the shared screen contract |
|
|
829
832
|
| Add per-row commands to a generated list page | page-local `listRowActions.js`, usually calling `useCommand()`-backed composables | The shared list screen renders action chrome; the page owns explicit mutation behavior |
|
|
@@ -1836,6 +1839,19 @@ Touch:
|
|
|
1836
1839
|
|
|
1837
1840
|
Do not hide permission rules inside client components.
|
|
1838
1841
|
|
|
1842
|
+
### "I need a manager to see assigned records and descendants."
|
|
1843
|
+
|
|
1844
|
+
Read [Row Policies](/guide/generators/row-policies).
|
|
1845
|
+
|
|
1846
|
+
Touch:
|
|
1847
|
+
|
|
1848
|
+
- a server-only policy module in the resource-owning package
|
|
1849
|
+
- the owning provider's `createJsonRestResourceScopeOptions(...)` call
|
|
1850
|
+
- a domain-owned visibility contribution seam when another package grants access
|
|
1851
|
+
- focused pagination, count, missing-identity, child-resource, and dependency-direction tests
|
|
1852
|
+
|
|
1853
|
+
Do not filter the JSON:API document in `service.js`, accept visible ids from the client, or make the resource-owning package depend on every package that can grant access.
|
|
1854
|
+
|
|
1839
1855
|
## Final mental model
|
|
1840
1856
|
|
|
1841
1857
|
A generated CRUD is not a monolith.
|
|
@@ -41,6 +41,7 @@ For CRUD work, there are also two guide layers on purpose:
|
|
|
41
41
|
|
|
42
42
|
- [CRUD Generators](/guide/generators/crud-generators) teaches the end-to-end workflow for generating a working CRUD
|
|
43
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
|
|
44
45
|
|
|
45
46
|
The intended reading order is:
|
|
46
47
|
|
|
@@ -61,3 +62,4 @@ That keeps the runtime reusable without hiding your real app UI behind opaque fr
|
|
|
61
62
|
- [UI Generators](/guide/generators/ui-generators) covers the non-CRUD workflow built around `ui-generator`
|
|
62
63
|
- [CRUD Generators](/guide/generators/crud-generators) covers the combined server-and-UI CRUD workflow using `contacts`, nested `addresses`, and in-page `comments`
|
|
63
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
|
|
@@ -0,0 +1,545 @@
|
|
|
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 dependsOn = [
|
|
255
|
+
"runtime.actions",
|
|
256
|
+
"runtime.database",
|
|
257
|
+
"auth.policy.fastify",
|
|
258
|
+
"local.main",
|
|
259
|
+
"json-rest-api.core"
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
register(app) {
|
|
263
|
+
app.instance(
|
|
264
|
+
ORGANISATION_UNIT_VISIBILITY,
|
|
265
|
+
createOrganisationUnitVisibility()
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
registerOrganisationUnitVisibility(app, {
|
|
269
|
+
id: "organisation-unit-members",
|
|
270
|
+
apply: applyOrganisationUnitMemberVisibility
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Existing repository, service, and action registration stays here.
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async boot(app) {
|
|
277
|
+
const contributions = app.make(ORGANISATION_UNIT_VISIBILITY).seal();
|
|
278
|
+
const rowPolicy = createOrganisationUnitRowPolicy(contributions);
|
|
279
|
+
const api = app.make(INTERNAL_JSON_REST_API);
|
|
280
|
+
|
|
281
|
+
await addResourceIfMissing(
|
|
282
|
+
api,
|
|
283
|
+
"organisationUnits",
|
|
284
|
+
createJsonRestResourceScopeOptions(resource, {
|
|
285
|
+
rowPolicy,
|
|
286
|
+
writeSerializers: {
|
|
287
|
+
"datetime-utc": toDatabaseDateTimeUtc
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Existing route registration stays here.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
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`.
|
|
300
|
+
|
|
301
|
+
### The composed policy
|
|
302
|
+
|
|
303
|
+
Each contribution adds one grouped SQL grant and returns `true`, or returns `false` when it grants nothing for the current context:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
function createOrganisationUnitRowPolicy(contributions = []) {
|
|
307
|
+
return function organisationUnitRowPolicy(policyContext) {
|
|
308
|
+
if (contributions.length === 0) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
policyContext.query.where(function organisationUnitVisibility() {
|
|
313
|
+
for (const contribution of contributions) {
|
|
314
|
+
this.orWhere(function oneVisibilityGrant() {
|
|
315
|
+
const decision = contribution.apply({
|
|
316
|
+
...policyContext,
|
|
317
|
+
query: this
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (decision === false) {
|
|
321
|
+
this.whereRaw("1 = 0");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (decision !== true) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Organisation-unit visibility contribution ${contribution.id} must return true or false.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return true;
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
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.
|
|
339
|
+
|
|
340
|
+
### The safety provider
|
|
341
|
+
|
|
342
|
+
The safety package depends on organisation-units and registers its grant:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
class SafetyProvider {
|
|
346
|
+
static id = "safety.core";
|
|
347
|
+
|
|
348
|
+
static dependsOn = ["crud.organisation_units"];
|
|
349
|
+
|
|
350
|
+
register(app) {
|
|
351
|
+
registerOrganisationUnitVisibility(app, {
|
|
352
|
+
id: "safety-manager-descendants",
|
|
353
|
+
apply: applySafetyManagerDescendantVisibility
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Existing safety registrations stay here.
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The organisation-units package never imports safety. Installing safety adds the grant; omitting safety leaves that grant absent.
|
|
362
|
+
|
|
363
|
+
The package descriptor dependency must point in the same direction as the provider dependency: safety declares organisation-units, and organisation-units does not declare safety.
|
|
364
|
+
|
|
365
|
+
## Descendant visibility with a recursive CTE
|
|
366
|
+
|
|
367
|
+
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`.
|
|
368
|
+
|
|
369
|
+
For a safety manager assigned to one unit:
|
|
370
|
+
|
|
371
|
+
```text
|
|
372
|
+
assigned unit
|
|
373
|
+
├── child
|
|
374
|
+
│ └── grandchild
|
|
375
|
+
└── child
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
the CTE produces the assigned unit and all descendants. The outer resource query keeps only organisation-unit rows whose id is in that result.
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
function applySafetyManagerDescendantVisibility({
|
|
382
|
+
query,
|
|
383
|
+
context,
|
|
384
|
+
db,
|
|
385
|
+
column
|
|
386
|
+
}) {
|
|
387
|
+
const userId = context.visibilityContext?.userId;
|
|
388
|
+
const workspaceId = context.visibilityContext?.scopeOwnerId;
|
|
389
|
+
|
|
390
|
+
if (!userId || !workspaceId) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const visibleUnitIds = db
|
|
395
|
+
.withRecursive("visible_units", ["unit_id"], (cte) => {
|
|
396
|
+
cte
|
|
397
|
+
.select("assignment.organisation_unit_id as unit_id")
|
|
398
|
+
.from("safety_manager_organisation_units as assignment")
|
|
399
|
+
.where("assignment.user_id", userId)
|
|
400
|
+
.where("assignment.workspace_id", workspaceId)
|
|
401
|
+
.unionAll(function descendantUnits() {
|
|
402
|
+
this
|
|
403
|
+
.select("child.id as unit_id")
|
|
404
|
+
.from("organisation_units as child")
|
|
405
|
+
.join(
|
|
406
|
+
"visible_units as visible",
|
|
407
|
+
"child.parent_id",
|
|
408
|
+
"visible.unit_id"
|
|
409
|
+
)
|
|
410
|
+
.where("child.workspace_id", workspaceId);
|
|
411
|
+
});
|
|
412
|
+
})
|
|
413
|
+
.select("unit_id")
|
|
414
|
+
.from("visible_units");
|
|
415
|
+
|
|
416
|
+
query.whereIn(column("id"), visibleUnitIds);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
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`.
|
|
422
|
+
|
|
423
|
+
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.
|
|
424
|
+
|
|
425
|
+
At minimum, index:
|
|
426
|
+
|
|
427
|
+
- `organisation_units.id`
|
|
428
|
+
- `organisation_units.parent_id`
|
|
429
|
+
- the safety assignment's `user_id`, `workspace_id`, and `organisation_unit_id` access path
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
## Missing identity and partial grants
|
|
434
|
+
|
|
435
|
+
Mandatory visibility fails closed:
|
|
436
|
+
|
|
437
|
+
- a missing user or workspace makes the safety contribution return `false`
|
|
438
|
+
- a contribution returning `false` becomes an always-false branch; other contributions can still grant access
|
|
439
|
+
- no installed contributions makes the resource policy return `false`, hiding every row
|
|
440
|
+
- an omitted or invalid contribution return throws instead of silently removing the policy
|
|
441
|
+
- a policy error fails the request; do not catch it and retry without the policy
|
|
442
|
+
|
|
443
|
+
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.”
|
|
444
|
+
|
|
445
|
+
## Children, includes, and relationship operations
|
|
446
|
+
|
|
447
|
+
The target resource's row policy applies when JSON REST loads included records, relationship identifiers, nested includes, and relationship targets used during write validation.
|
|
448
|
+
|
|
449
|
+
For a self-referencing organisation-unit relationship, the child target is the same `organisationUnits` resource, so the same row policy applies.
|
|
450
|
+
|
|
451
|
+
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.
|
|
452
|
+
|
|
453
|
+
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.
|
|
454
|
+
|
|
455
|
+
## Reads and writes are different contracts
|
|
456
|
+
|
|
457
|
+
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.
|
|
458
|
+
|
|
459
|
+
It does not:
|
|
460
|
+
|
|
461
|
+
- stamp ownership values on creates
|
|
462
|
+
- replace generated ownership autofilters
|
|
463
|
+
- decide whether an action is allowed
|
|
464
|
+
- replace service-level domain validation
|
|
465
|
+
|
|
466
|
+
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.
|
|
467
|
+
|
|
468
|
+
## Public API and query contracts do not change
|
|
469
|
+
|
|
470
|
+
Row policies are mandatory server behavior. They do not add a public list-filter field and do not change:
|
|
471
|
+
|
|
472
|
+
- filter definitions
|
|
473
|
+
- URLs or query-string serialization
|
|
474
|
+
- generated repository method names
|
|
475
|
+
- JSON:API document shapes
|
|
476
|
+
- active-filter chips
|
|
477
|
+
- application call sites
|
|
478
|
+
|
|
479
|
+
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.
|
|
480
|
+
|
|
481
|
+
## Testing the actual failure
|
|
482
|
+
|
|
483
|
+
Use interleaved visible and hidden rows. For example:
|
|
484
|
+
|
|
485
|
+
```text
|
|
486
|
+
visible root
|
|
487
|
+
hidden root
|
|
488
|
+
visible child
|
|
489
|
+
hidden child
|
|
490
|
+
visible grandchild
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
With page size two, assert:
|
|
494
|
+
|
|
495
|
+
- page one contains the first two visible rows
|
|
496
|
+
- page two contains the remaining visible row
|
|
497
|
+
- the total is three
|
|
498
|
+
- a cursor from page one reaches only the remaining visible row
|
|
499
|
+
- direct access to a hidden record behaves as not found
|
|
500
|
+
- missing trusted identity returns no rows or the documented context error
|
|
501
|
+
- a self-referencing child load does not expose hidden units
|
|
502
|
+
- a separately scoped child resource applies its own policy
|
|
503
|
+
- clearing client filters does not remove mandatory visibility
|
|
504
|
+
|
|
505
|
+
Also test the package graph and provider lifecycle:
|
|
506
|
+
|
|
507
|
+
- safety depends on organisation-units
|
|
508
|
+
- organisation-units does not depend on safety
|
|
509
|
+
- safety contributes during `register()`
|
|
510
|
+
- organisation-units seals contributions during `boot()`
|
|
511
|
+
- duplicate contribution ids fail startup
|
|
512
|
+
- late contributions fail after the visibility object is sealed
|
|
513
|
+
|
|
514
|
+
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.
|
|
515
|
+
|
|
516
|
+
## AnyAPI comes later
|
|
517
|
+
|
|
518
|
+
`json-rest-api@1.0.25` can apply row policies with `RestApiAnyapiKnexPlugin`. JSKIT's current `internal.json-rest-api` host installs `RestApiKnexPlugin`, so the code in this chapter targets normal tables.
|
|
519
|
+
|
|
520
|
+
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.
|
|
521
|
+
|
|
522
|
+
When JSKIT adds an explicit AnyAPI host:
|
|
523
|
+
|
|
524
|
+
1. install `RowPolicyPlugin` in that host after its AnyAPI storage plugin
|
|
525
|
+
2. preserve the same server-only `rowPolicy` resource option
|
|
526
|
+
3. reuse policies that only need logical `column()` and `value()` translation
|
|
527
|
+
4. implement recursive canonical-storage queries explicitly from the AnyAPI descriptor and storage adapter
|
|
528
|
+
5. run the domain regression against the AnyAPI host as a separate supported path
|
|
529
|
+
|
|
530
|
+
That is a separate storage implementation. It does not wrap or fall back to the normal-table query.
|
|
531
|
+
|
|
532
|
+
## Review checklist
|
|
533
|
+
|
|
534
|
+
- The resource-owning provider passes a server-only `rowPolicy` function.
|
|
535
|
+
- Generated applications do not install `RowPolicyPlugin` themselves.
|
|
536
|
+
- Ordinary client filters remain client-controlled; mandatory visibility does not.
|
|
537
|
+
- Every policy returns `true`, returns `false`, or throws.
|
|
538
|
+
- Every policy-owned `OR` is grouped.
|
|
539
|
+
- Trusted identity comes from execution context, not query parameters.
|
|
540
|
+
- Visibility SQL runs before sorting, count, and pagination.
|
|
541
|
+
- The resource owner does not import packages that merely grant extra visibility.
|
|
542
|
+
- Contribution state belongs to the application container and is sealed before requests.
|
|
543
|
+
- Self-referencing children use the same resource policy; separate child resources declare their own.
|
|
544
|
+
- Action permissions and autofilter write stamping remain in place.
|
|
545
|
+
- Normal Knex behavior is implemented and tested without speculative AnyAPI branches.
|
package/package.json
CHANGED
package/patterns/INDEX.md
CHANGED
|
@@ -38,6 +38,8 @@ How to use it:
|
|
|
38
38
|
- `server-search.md`
|
|
39
39
|
- `actualField`, `storage.writeSerializer`, `storage.virtual`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`, datetime write serialization
|
|
40
40
|
- `crud-repository-mapping.md`
|
|
41
|
+
- row policy, permission filtering, visibility before pagination, recursive CTE, descendants, package cycle, `RowPolicyPlugin`
|
|
42
|
+
- `row-policies.md`
|
|
41
43
|
|
|
42
44
|
## Current Patterns
|
|
43
45
|
|
|
@@ -55,3 +57,4 @@ How to use it:
|
|
|
55
57
|
- [filters.md](./filters.md)
|
|
56
58
|
- [server-search.md](./server-search.md)
|
|
57
59
|
- [crud-repository-mapping.md](./crud-repository-mapping.md)
|
|
60
|
+
- [row-policies.md](./row-policies.md)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Row Policies
|
|
2
|
+
|
|
3
|
+
Use when:
|
|
4
|
+
|
|
5
|
+
- mandatory record visibility needs `OR`, `EXISTS`, joins, memberships, or hierarchy traversal
|
|
6
|
+
- filtering after pagination produces empty or incomplete pages
|
|
7
|
+
- another package grants access to a resource but a reverse dependency would create a cycle
|
|
8
|
+
- the user mentions `RowPolicyPlugin`, recursive CTEs, descendants, or permission-filtered counts
|
|
9
|
+
|
|
10
|
+
Read first:
|
|
11
|
+
|
|
12
|
+
- `packages/agent-docs/site/guide/generators/row-policies.md`
|
|
13
|
+
- the owning CRUD provider and shared resource
|
|
14
|
+
- the generated CRUD repository and service templates
|
|
15
|
+
- `packages/crud-server-generator/test/crudService.test.js`
|
|
16
|
+
|
|
17
|
+
Default JSKIT pattern:
|
|
18
|
+
|
|
19
|
+
1. Keep public search and structured filters in the existing list-filter contract.
|
|
20
|
+
2. Keep direct user/workspace ownership and write stamping in autofilter.
|
|
21
|
+
3. Put complex mandatory read visibility in a server-only row policy.
|
|
22
|
+
4. Pass the policy through `createJsonRestResourceScopeOptions(resource, { rowPolicy })` in the owning provider.
|
|
23
|
+
5. Derive identity from trusted execution context.
|
|
24
|
+
6. Group every policy-owned `OR` branch.
|
|
25
|
+
7. Return `true` after adding the predicate, return `false` to deny all rows, or throw for invalid trusted context.
|
|
26
|
+
8. Keep action permissions for writes; visibility is not mutation authorization.
|
|
27
|
+
|
|
28
|
+
Package composition:
|
|
29
|
+
|
|
30
|
+
- the package that owns the resource also owns the row policy
|
|
31
|
+
- a package that grants extra visibility depends on the resource owner
|
|
32
|
+
- the resource owner does not import grant packages
|
|
33
|
+
- create one application-scoped visibility object during the owner's `register()`
|
|
34
|
+
- grant packages add contributions during their `register()`
|
|
35
|
+
- the owner seals the contributions and builds the policy during `boot()`
|
|
36
|
+
- do not use module-global mutable registries
|
|
37
|
+
|
|
38
|
+
Child-resource rules:
|
|
39
|
+
|
|
40
|
+
- a self-referencing relationship targets the same resource and receives the same row policy
|
|
41
|
+
- a separate child resource needs its own policy when it has an independent visibility rule
|
|
42
|
+
- never repair includes or child lists with response filtering
|
|
43
|
+
|
|
44
|
+
Testing:
|
|
45
|
+
|
|
46
|
+
- interleave visible and hidden rows so paginate-then-filter cannot accidentally pass
|
|
47
|
+
- assert first page, later page, total count, and cursor behavior
|
|
48
|
+
- assert missing identity fails closed
|
|
49
|
+
- assert hidden direct records behave as not found
|
|
50
|
+
- assert children and includes do not leak records
|
|
51
|
+
- assert package and provider dependencies stay one-way
|
|
52
|
+
|
|
53
|
+
AnyAPI boundary:
|
|
54
|
+
|
|
55
|
+
- the current JSKIT internal JSON REST host uses normal Knex tables
|
|
56
|
+
- do not add speculative backend detection or fallback code
|
|
57
|
+
- add an explicit AnyAPI host and domain regression before claiming JSKIT AnyAPI support for a policy
|
|
58
|
+
|
|
59
|
+
Avoid:
|
|
60
|
+
|
|
61
|
+
- filtering `document.data` in `service.js`
|
|
62
|
+
- accepting visible ids or hierarchy roots from client query parameters
|
|
63
|
+
- installing `RowPolicyPlugin` in generated applications
|
|
64
|
+
- adding a default policy file to every generated CRUD
|
|
65
|
+
- returning a Knex builder or promise from a synchronous grouped contribution
|
|
66
|
+
- swallowing a policy error and retrying without mandatory visibility
|
|
@@ -26,7 +26,7 @@ Exports
|
|
|
26
26
|
- `buildJsonRestQueryParams(resourceType = "", query = {}, { include = undefined } = {})`
|
|
27
27
|
- `createJsonApiInputRecord(resourceType = "", attributes = {}, { id = null, relationships = null, resource = null } = {})`
|
|
28
28
|
- `createJsonApiRelationship(resourceType = "", id = null)`
|
|
29
|
-
- `createJsonRestResourceScopeOptions(resource = {}, { writeSerializers = {}, normalizeId = null, searchSchema = null, queryFields = null } = {})`
|
|
29
|
+
- `createJsonRestResourceScopeOptions(resource = {}, { writeSerializers = {}, normalizeId = null, rowPolicy = undefined, searchSchema = null, queryFields = null } = {})`
|
|
30
30
|
- `createJsonRestContext(context = null)`
|
|
31
31
|
- `extractJsonRestCollectionRows(payload = null)`
|
|
32
32
|
- `isJsonRestResourceMissingError(error = null)`
|
|
@@ -785,6 +785,9 @@ Exports
|
|
|
785
785
|
- `createVirtualModuleSource(clientModules = [])`
|
|
786
786
|
- `resolveClientOptimizeIncludeSpecifiers(clientModules = [], excludeSpecifiers = [])`
|
|
787
787
|
- `resolveClientOptimizeExcludeSpecifiers(clientModules = [])`
|
|
788
|
+
- `resolveCanonicalLocalPackageId(resolvedId, localPackage)`
|
|
789
|
+
- `resolveLocalPackageForSpecifier(source, localPackages = [])`
|
|
790
|
+
- `resolveLocalPackageSources({ appRoot, lockPath })`
|
|
788
791
|
- `resolveLocalScopeOptimizeExcludeSpecifiers(localScopePackageIds = [])`
|
|
789
792
|
- `resolveInstalledClientPackageIds(options)`
|
|
790
793
|
- `resolveLocalScopePackageIds({ appRoot, lockPath })`
|
|
@@ -794,6 +797,8 @@ Local functions
|
|
|
794
797
|
- `isLocalScopePackageId(value)`
|
|
795
798
|
- `readJsonFile(filePath, fallback)`
|
|
796
799
|
- `hasClientExport(packageJson)`
|
|
800
|
+
- `isPathInsideRoot(rootPath, candidatePath)`
|
|
801
|
+
- `splitSpecifierSuffix(source)`
|
|
797
802
|
- `normalizeClientModuleDescriptors(value)`
|
|
798
803
|
- `resolveClientRuntimeDedupeSpecifiers(userResolveConfig = {})`
|
|
799
804
|
|
|
@@ -57,6 +57,17 @@ Local functions
|
|
|
57
57
|
- `isActionDisabled(action = {})`
|
|
58
58
|
- `isActionExecuting(action = {})`
|
|
59
59
|
|
|
60
|
+
### `src/client/components/CrudListDateFilterControl.vue`
|
|
61
|
+
Exports
|
|
62
|
+
- None
|
|
63
|
+
Local functions
|
|
64
|
+
- `restoreFocus()`
|
|
65
|
+
- `openMenu()`
|
|
66
|
+
- `closeMenu()`
|
|
67
|
+
- `clearDate()`
|
|
68
|
+
- `selectDate(value)`
|
|
69
|
+
- `handleActivatorKeydown(event)`
|
|
70
|
+
|
|
60
71
|
### `src/client/components/CrudListFilterSurface.vue`
|
|
61
72
|
Exports
|
|
62
73
|
- None
|
|
@@ -491,6 +502,7 @@ Local functions
|
|
|
491
502
|
- `applyPresetFilterValue(values, filter = {}, rawValue)`
|
|
492
503
|
- `createQueryParams(values, filterEntries = [])`
|
|
493
504
|
- `resolveAtomicValueLabel(filter = {}, value = "", labelResolvers = {})`
|
|
505
|
+
- `formatDefaultChipLabel(filter = {}, chipValue, labelResolvers = {})`
|
|
494
506
|
|
|
495
507
|
### `src/client/composables/useCrudListParentTitle.js`
|
|
496
508
|
Exports
|
|
@@ -646,6 +658,13 @@ Exports
|
|
|
646
658
|
- `requireBoolean(value, label = "value", owner = "Contract")`
|
|
647
659
|
- `requireFunction(value, label = "value", owner = "Contract")`
|
|
648
660
|
|
|
661
|
+
### `src/client/support/crudListDateFilterSupport.js`
|
|
662
|
+
Exports
|
|
663
|
+
- `parseDateOnlyValue(value = "")`
|
|
664
|
+
- `formatDateOnlyValue(value = null)`
|
|
665
|
+
- `formatDateOnlyDisplay(value = "", { locale = undefined } = {})`
|
|
666
|
+
- `formatCrudListDateFilterChipLabel(filter = {}, rawValue, { locale = undefined } = {})`
|
|
667
|
+
|
|
649
668
|
### `src/shared/toolsOutletContracts.js`
|
|
650
669
|
Exports
|
|
651
670
|
- `HOME_COG_OUTLET`
|
|
@@ -673,3 +692,17 @@ Exports
|
|
|
673
692
|
### `package.descriptor.mjs`
|
|
674
693
|
Exports
|
|
675
694
|
- None
|
|
695
|
+
|
|
696
|
+
### fixtures
|
|
697
|
+
|
|
698
|
+
### `fixtures/crud-list-date-filters/App.vue`
|
|
699
|
+
Exports
|
|
700
|
+
- None
|
|
701
|
+
|
|
702
|
+
### `fixtures/crud-list-date-filters/main.js`
|
|
703
|
+
Exports
|
|
704
|
+
- None
|
|
705
|
+
|
|
706
|
+
### `fixtures/crud-list-date-filters/vite.config.mjs`
|
|
707
|
+
Exports
|
|
708
|
+
- None
|
|
@@ -553,10 +553,12 @@ Exports
|
|
|
553
553
|
- `normalizeText(value = "")`
|
|
554
554
|
- `isTruthyFlag(rawValue = "")`
|
|
555
555
|
- `runExternalCommand(command, args = [], { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
|
|
556
|
+
- `runExternalCommandAsync(command, args = [], { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
|
|
556
557
|
- `runExternalShellCommand(commandText, { cwd = "", env = {}, stdout, stderr, quiet = false, createCliError } = {})`
|
|
557
558
|
- `formatUtcReleaseTimestamp(date = new Date())`
|
|
558
559
|
- `resolveLocalJskitBin(appRoot = "")`
|
|
559
560
|
- `runLocalJskit(appRoot, args = [], { stdout, stderr, createCliError, quiet = false } = {})`
|
|
561
|
+
- `runLocalJskitAsync(appRoot, args = [], { stdout, stderr, createCliError, quiet = false } = {})`
|
|
560
562
|
- `resolveLocalRepoRoot({ appRoot = "", explicitRepoRoot = "" } = {})`
|
|
561
563
|
- `discoverLocalPackageMap(repoRoot = "")`
|
|
562
564
|
- `linkPackageBinEntries({ appRoot, packageDirName, sourceDir, stdout } = {})`
|
|
@@ -570,12 +572,27 @@ Exports
|
|
|
570
572
|
|
|
571
573
|
### `src/server/commandHandlers/appCommands/updatePackages.js`
|
|
572
574
|
Exports
|
|
575
|
+
- `formatElapsedTime(elapsedMilliseconds = 0)`
|
|
573
576
|
- `runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr })`
|
|
577
|
+
- `runWithProgress(task, { activity, progressIntervalMs = PROGRESS_INTERVAL_MS, stdout, step } = {})`
|
|
574
578
|
Local functions
|
|
575
579
|
- `collectJskitPackageNames(packageMap = {})`
|
|
576
|
-
- `
|
|
580
|
+
- `collectManifestJskitPackageNames(packageJson = {})`
|
|
581
|
+
- `resolveExactVersion(packageName = "", rawVersion = "", createCliError)`
|
|
582
|
+
- `resolveMajorRange(packageName = "", version = "", createCliError)`
|
|
577
583
|
- `resolveRegistryArgs(registryUrl = "")`
|
|
578
|
-
- `resolveInstallSpecs(packageNames = [],
|
|
584
|
+
- `resolveInstallSpecs(packageNames = [], latestVersions = new Map())`
|
|
585
|
+
- `hasNpmWorkspaces(packageJson = {})`
|
|
586
|
+
- `readJson(filePath)`
|
|
587
|
+
- `readOptionalFile(filePath)`
|
|
588
|
+
- `resolveWorkspaceDirectories({ appRoot, createCliError, packageJson, stderr, stdout })`
|
|
589
|
+
- `descriptorJskitPackageNames(source = "")`
|
|
590
|
+
- `loadWorkspacePackages(workspaceDirectories = [])`
|
|
591
|
+
- `resolveLatestVersions(packageNames = [], latestVersions = new Map(), { appRoot, createCliError, registryArgs, stderr, stdout })`
|
|
592
|
+
- `updateWorkspaceManifest(packageJson = {}, latestVersions = new Map(), createCliError)`
|
|
593
|
+
- `updateWorkspaceDescriptor(source = "", latestVersions = new Map(), createCliError)`
|
|
594
|
+
- `synchronizeWorkspacePackageSpecs({ appRoot, createCliError, dryRun, latestVersions, packageJson, registryArgs, stderr, stdout })`
|
|
595
|
+
- `updateRootPackages({ appRoot, createCliError, dryRun, latestVersions, packageJson, registryArgs, stderr, stdout })`
|
|
579
596
|
|
|
580
597
|
### `src/server/commandHandlers/appCommands/verify.js`
|
|
581
598
|
Exports
|