@jskit-ai/agent-docs 0.1.87 → 0.1.90
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/database-layer.md +7 -3
- package/guide/agent/generators/advanced-cruds.md +10 -1
- package/guide/agent/generators/crud-generators.md +67 -3
- package/guide/agent/generators/row-policies.md +1 -1
- package/package.json +1 -1
- package/patterns/crud-scaffolding.md +11 -0
- package/reference/autogen/packages/crud-server-generator.md +10 -4
|
@@ -197,11 +197,15 @@ In JSKIT persistence code, **visibility** means "which rows should this request
|
|
|
197
197
|
- `public`
|
|
198
198
|
- the record is not scoped by owner columns
|
|
199
199
|
- `workspace`
|
|
200
|
-
- the record belongs to one workspace
|
|
200
|
+
- the record belongs to one workspace through the exact reserved column `workspace_id`
|
|
201
201
|
- `user`
|
|
202
|
-
- the record belongs to one user
|
|
202
|
+
- the record belongs to one user through the exact reserved column `user_id`
|
|
203
203
|
- `workspace_user`
|
|
204
|
-
- the record belongs to one workspace and one user
|
|
204
|
+
- the record belongs to one workspace and one user through both reserved columns
|
|
205
|
+
|
|
206
|
+
Only `workspace_id` and `user_id` carry this standard ownership contract. Specifically named foreign keys such as `recipient_user_id`, `created_by_user_id`, and `assignee_user_id` describe domain relationships; they are not alternate owner columns. Keep both fields when a row has an owner and a separate related actor, and never rename the relationship to an owner column merely to make a tool accept the schema.
|
|
207
|
+
|
|
208
|
+
The selected ownership filter must match the direct reserved columns exactly. A table with only `workspace_id` is `workspace`; a table with only `user_id` is `user`; and a table with both is `workspace_user`. A declaration cannot override or ignore either column.
|
|
205
209
|
|
|
206
210
|
That is why the shared helpers exist. Repositories should not have to re-implement the same ownership rules by hand every time they filter a query or build an insert payload.
|
|
207
211
|
|
|
@@ -33,7 +33,8 @@ npx jskit generate crud-server-generator scaffold \
|
|
|
33
33
|
--namespace contacts \
|
|
34
34
|
--surface admin \
|
|
35
35
|
--ownership-filter workspace \
|
|
36
|
-
--table-name contacts
|
|
36
|
+
--table-name contacts \
|
|
37
|
+
--grant-role member
|
|
37
38
|
|
|
38
39
|
npx jskit generate crud-ui-generator crud \
|
|
39
40
|
w/[workspaceSlug]/admin/contacts \
|
|
@@ -140,6 +141,8 @@ So the distinction is:
|
|
|
140
141
|
|
|
141
142
|
That is why `--internal` is not a permissions shortcut and not a UI setting. It is a server-route exposure choice on top of the same CRUD ownership model.
|
|
142
143
|
|
|
144
|
+
The workspace role-grant decision is separate too. Every workspace-required generation must choose `--grant-role <role-id>` or `--no-role-grant`; there is no implicit role. An application that assigns generated CRUD permissions to `member` uses `--grant-role member`. In particular, `--internal` does not imply `--no-role-grant`.
|
|
145
|
+
|
|
143
146
|
### Ownership controls which owner columns are expected
|
|
144
147
|
|
|
145
148
|
The repository layer ultimately applies visibility through the standard owner columns:
|
|
@@ -147,6 +150,12 @@ The repository layer ultimately applies visibility through the standard owner co
|
|
|
147
150
|
- `workspace_id`
|
|
148
151
|
- `user_id`
|
|
149
152
|
|
|
153
|
+
Those names are exact and reserved. `workspace_id` is workspace ownership; `user_id` is user ownership; both together are workspace-user ownership. Other foreign keys such as `recipient_user_id`, `created_by_user_id`, and `assignee_user_id` remain domain relationships and must not be treated as ownership aliases.
|
|
154
|
+
|
|
155
|
+
The generated ownership filter must match that exact set of reserved columns. Neither an explicit filter nor generated metadata can override what those direct columns mean.
|
|
156
|
+
|
|
157
|
+
If a row is workspace-owned but refers to a recipient, keep both concepts explicit: use `workspace_id` for ownership and `recipient_user_id` for the relationship. Renaming the relationship to `user_id` would opt the field into JSKIT's hidden owner-field and user-filtering behavior.
|
|
158
|
+
|
|
150
159
|
That means the generated CRUD behaves like this:
|
|
151
160
|
|
|
152
161
|
- `public`
|
|
@@ -99,6 +99,27 @@ The supported values are:
|
|
|
99
99
|
| `workspace` | rows belong to one workspace | `workspace_id` |
|
|
100
100
|
| `workspace_user` | rows belong to one workspace and one user together | `workspace_id` and `user_id` |
|
|
101
101
|
|
|
102
|
+
### Ownership column names are exact and reserved
|
|
103
|
+
|
|
104
|
+
JSKIT ownership is explicit and materialized. Only these exact database columns carry the standard generated ownership contract:
|
|
105
|
+
|
|
106
|
+
- `workspace_id`
|
|
107
|
+
- `user_id`
|
|
108
|
+
|
|
109
|
+
They are not ordinary foreign-key names. Generated CRUD infrastructure uses them for visibility filtering, create-time owner stamping, and hidden owner-field handling.
|
|
110
|
+
|
|
111
|
+
Other foreign keys are domain relationships, even when their names end in `_user_id` or `_workspace_id`. For example:
|
|
112
|
+
|
|
113
|
+
- `recipient_user_id` identifies a recipient
|
|
114
|
+
- `created_by_user_id` records an author
|
|
115
|
+
- `assignee_user_id` identifies an assignee
|
|
116
|
+
|
|
117
|
+
Those fields do not become ownership aliases merely because they point to a user-owned table.
|
|
118
|
+
|
|
119
|
+
The ownership filter must match the direct reserved columns exactly. A table with only `workspace_id` is `workspace`; one with only `user_id` is `user`; and one with both is `workspace_user`. Neither the CLI declaration nor generated metadata overrides those columns.
|
|
120
|
+
|
|
121
|
+
Decide ownership first, select the matching ownership filter, and then model other actors with specifically named foreign keys. A workspace-owned notification outbox item that targets a user normally has `workspace_id` for ownership and `recipient_user_id` for the recipient relationship. Do not rename `recipient_user_id` to `user_id` merely to satisfy a generator or Doctor check; that would change the resource's ownership semantics.
|
|
122
|
+
|
|
102
123
|
### What each value means
|
|
103
124
|
|
|
104
125
|
#### `public`
|
|
@@ -257,7 +278,8 @@ npx jskit generate crud-server-generator scaffold \
|
|
|
257
278
|
--namespace contacts \
|
|
258
279
|
--surface admin \
|
|
259
280
|
--ownership-filter workspace \
|
|
260
|
-
--table-name contacts
|
|
281
|
+
--table-name contacts \
|
|
282
|
+
--grant-role member
|
|
261
283
|
```
|
|
262
284
|
|
|
263
285
|
This creates an app-local package under `packages/contacts/`.
|
|
@@ -278,6 +300,46 @@ Use that when:
|
|
|
278
300
|
|
|
279
301
|
Do **not** use `--internal` as a substitute for ownership or permissions design. It is only about whether the public HTTP CRUD routes exist.
|
|
280
302
|
|
|
303
|
+
### Choose workspace role grants explicitly
|
|
304
|
+
|
|
305
|
+
Workspace-required CRUD actions use the normal named permission ids:
|
|
306
|
+
|
|
307
|
+
- `crud.<namespace>.list`
|
|
308
|
+
- `crud.<namespace>.view`
|
|
309
|
+
- `crud.<namespace>.create`
|
|
310
|
+
- `crud.<namespace>.update`
|
|
311
|
+
- `crud.<namespace>.delete`
|
|
312
|
+
|
|
313
|
+
The generator never guesses which role should receive all five permissions. Every workspace-required CRUD command must choose one of these options:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
--grant-role administrator
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
or:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
--no-role-grant
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`--grant-role` must name a configured role. `--no-role-grant` keeps the generated actions and their permission requirements but leaves the app's role catalog unchanged. The options are mutually exclusive.
|
|
326
|
+
|
|
327
|
+
To assign the generated permissions to `member`, make that decision explicit:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
--grant-role member
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
This decision is independent of `--internal`. For an internal, workspace-owned persistence resource whose generated actions should not be assigned to any actor role, use both:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
--ownership-filter workspace \
|
|
337
|
+
--internal \
|
|
338
|
+
--no-role-grant
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
If neither explicit grant option is supplied, generation fails during preflight before writing files, even when a `member` role exists.
|
|
342
|
+
|
|
281
343
|
Before generating anything, decide these with the developer:
|
|
282
344
|
|
|
283
345
|
- which operations are allowed for this CRUD
|
|
@@ -420,7 +482,8 @@ npx jskit generate crud-server-generator scaffold \
|
|
|
420
482
|
--namespace addresses \
|
|
421
483
|
--surface admin \
|
|
422
484
|
--ownership-filter workspace \
|
|
423
|
-
--table-name addresses
|
|
485
|
+
--table-name addresses \
|
|
486
|
+
--grant-role member
|
|
424
487
|
```
|
|
425
488
|
|
|
426
489
|
Then install the generated local package:
|
|
@@ -596,7 +659,8 @@ npx jskit generate crud-server-generator scaffold \
|
|
|
596
659
|
--namespace comments \
|
|
597
660
|
--surface admin \
|
|
598
661
|
--ownership-filter workspace \
|
|
599
|
-
--table-name comments
|
|
662
|
+
--table-name comments \
|
|
663
|
+
--grant-role member
|
|
600
664
|
```
|
|
601
665
|
|
|
602
666
|
Then install the generated local package:
|
|
@@ -515,7 +515,7 @@ The `json-rest-api` package owns exhaustive framework tests for pagination, coun
|
|
|
515
515
|
|
|
516
516
|
## AnyAPI comes later
|
|
517
517
|
|
|
518
|
-
`json-rest-api@1.0.
|
|
518
|
+
`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.
|
|
519
519
|
|
|
520
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
521
|
|
package/package.json
CHANGED
|
@@ -24,6 +24,9 @@ Rules:
|
|
|
24
24
|
- If `crud-server-generator` is going to own the CRUD, do not hand-write a separate CRUD migration for that table. The generator installs and manages the CRUD migration scaffold itself.
|
|
25
25
|
- Do not scaffold CRUD UI, hand-build CRUD routes, or hand-build CRUD endpoints before the server CRUD package and shared resource file exist.
|
|
26
26
|
- Treat the generated shared resource file as the canonical CRUD contract for later UI scaffolding and CRUD behavior changes.
|
|
27
|
+
- Treat the exact columns `workspace_id` and `user_id` as reserved JSKIT ownership columns. They are the only standard columns used for generated ownership filtering and create-time owner stamping.
|
|
28
|
+
- Treat other foreign keys such as `recipient_user_id`, `created_by_user_id`, `assignee_user_id`, and similarly specific names as domain relationships, not ownership aliases. Do not rename a relationship to `user_id` or `workspace_id` merely to satisfy tooling.
|
|
29
|
+
- Require the resolved ownership filter to match the reserved columns exactly: neither the filter nor the schema may silently add or omit `workspace_id` or `user_id` ownership.
|
|
27
30
|
- `feature-server-generator` is not the default lane for ordinary persisted entities. Use it for workflows or orchestration that sit on top of CRUD-owned tables, or for rare explicit non-CRUD exceptions.
|
|
28
31
|
- Generated CRUD UI must be compact-first. Lists need searchable cards on compact widths and tables only for medium/expanded layouts.
|
|
29
32
|
- Generated CRUD list screens need real loading, empty, and error states. Empty copy should name the resource, such as "No customers yet", and offer the create action when available.
|
|
@@ -41,6 +44,14 @@ Meaning of `--internal`:
|
|
|
41
44
|
- it keeps the generated repository, service, actions, provider, resource, and CRUD migration ownership chain
|
|
42
45
|
- it only suppresses public HTTP CRUD route registration
|
|
43
46
|
- it is not a substitute for ownership columns or action/route permissions
|
|
47
|
+
- it does not suppress generated action permission ids or decide which role receives them
|
|
48
|
+
|
|
49
|
+
Workspace role grants:
|
|
50
|
+
|
|
51
|
+
- every workspace-required CRUD generation must explicitly choose `--grant-role <role-id>` or `--no-role-grant`
|
|
52
|
+
- applications that assign generated CRUD permissions to `member` must say `--grant-role member`; fixed-role applications must name one of their real roles or choose no automatic grant
|
|
53
|
+
- `--grant-role` and `--no-role-grant` are permission-design choices independent of `--internal`
|
|
54
|
+
- never invent a `member` role only to satisfy the generator
|
|
44
55
|
|
|
45
56
|
When a weird-custom persistence lane is proposed:
|
|
46
57
|
|
|
@@ -21,13 +21,16 @@ Exports
|
|
|
21
21
|
- `resolveGenerationSnapshot({ appRoot, tableName, idColumnOption } = {})`
|
|
22
22
|
- `renderCanonicalResourceFieldSchema(column, { fieldContractEntry = null } = {})`
|
|
23
23
|
- `buildFieldContractEntries({ outputColumns = [], writableColumns = [], snapshot = {} } = {})`
|
|
24
|
-
- `resolveCrudGenerationSurfaceId({ appRoot, options } = {})`
|
|
24
|
+
- `resolveCrudGenerationSurfaceId({ appRoot, options, appConfig = null } = {})`
|
|
25
|
+
- `prepareInstallHook({ appRoot, packageOptions = {} } = {})`
|
|
25
26
|
- `__testables`
|
|
26
27
|
Local functions
|
|
27
28
|
- `resolveAllowedValues(schema = {}, fallbackValues = [])`
|
|
28
29
|
- `resolveGlobalScaffoldCache()`
|
|
29
30
|
- `asRecord(value)`
|
|
31
|
+
- `resolveBooleanFlagOption(options = {}, optionName = "")`
|
|
30
32
|
- `resolveInternalRouteOption(options = {})`
|
|
33
|
+
- `resolveNoRoleGrantOption(options = {})`
|
|
31
34
|
- `normalizeRequestedOwnershipFilter(value, { strict = false } = {})`
|
|
32
35
|
- `inferOwnershipFilterFromSnapshot(snapshot)`
|
|
33
36
|
- `assertOwnershipColumnsForFilter(snapshot, filter)`
|
|
@@ -36,7 +39,7 @@ Local functions
|
|
|
36
39
|
- `loadEnvFromApp(appRoot)`
|
|
37
40
|
- `createAppRequire(appRoot)`
|
|
38
41
|
- `importModuleFromApp(appRequire, moduleId, contextLabel)`
|
|
39
|
-
- `resolveCrudSurfaceRequiresWorkspace({ appRoot, options, surface = "" } = {})`
|
|
42
|
+
- `resolveCrudSurfaceRequiresWorkspace({ appRoot, options, surface = "", appConfig = null } = {})`
|
|
40
43
|
- `loadCrudAppConfig(appRoot = "")`
|
|
41
44
|
- `resolveSurfaceDefinitions(appConfig = {})`
|
|
42
45
|
- `resolveDefaultCrudSurfaceIdFromAppConfig(appConfig = {})`
|
|
@@ -84,8 +87,11 @@ Local functions
|
|
|
84
87
|
- `normalizeFieldMetaUiOptions(rawOptions = [])`
|
|
85
88
|
- `resolveEnumFieldMetaUiOptions(enumValues = [])`
|
|
86
89
|
- `buildCrudPermissionIds(namespace = "")`
|
|
90
|
+
- `resolveConfiguredRoleEntries(appConfig = {})`
|
|
91
|
+
- `formatAvailableRoleIds(roleEntries = [])`
|
|
92
|
+
- `resolveCrudPermissionGrantRole(appConfig = {}, options = {}, { requiresNamedPermissions = true } = {})`
|
|
87
93
|
- `normalizeCrudOperation(operation = "", context = "CRUD operation")`
|
|
88
|
-
- `renderRoleCatalogPermissionGrants(namespace = "", { requiresNamedPermissions = true } = {})`
|
|
94
|
+
- `renderRoleCatalogPermissionGrants(namespace = "", { requiresNamedPermissions = true, grantRoleId = "" } = {})`
|
|
89
95
|
- `renderActionPermissionSupport(namespace = "", { requiresNamedPermissions = true } = {})`
|
|
90
96
|
- `renderActionPermissionExpression(operation = "", { requiresNamedPermissions = true } = {})`
|
|
91
97
|
- `renderRouteWorkspaceSupportImports({ surfaceRequiresWorkspace = true } = {})`
|
|
@@ -96,7 +102,7 @@ Local functions
|
|
|
96
102
|
- `renderObjectSchemaDefinition(lines = [], { mode = "patch" } = {})`
|
|
97
103
|
- `renderActionInputExpressions({ surfaceRequiresWorkspace = true } = {})`
|
|
98
104
|
- `renderRouteValidatorConstants({ surfaceRequiresWorkspace = true } = {})`
|
|
99
|
-
- `buildReplacementsFromSnapshot({ namespace = "", snapshot, resolvedOwnershipFilter, surfaceRequiresWorkspace = true, surfaceId = "", routeInternal = false })`
|
|
105
|
+
- `buildReplacementsFromSnapshot({ namespace = "", snapshot, resolvedOwnershipFilter, surfaceRequiresWorkspace = true, surfaceId = "", routeInternal = false, permissionGrantRoleId = "" })`
|
|
100
106
|
- `resolveCrudGenerationTableName(options = {})`
|
|
101
107
|
- `createCacheKey({ appRoot, options })`
|
|
102
108
|
- `buildCrudTemplateContext(input = {})`
|