@jskit-ai/agent-docs 0.1.283 → 0.1.285
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guide/agent/app-extras/assistant.md +14 -2
- package/guide/agent/app-extras/realtime.md +38 -1
- package/guide/agent/app-setup/authentication.md +76 -14
- package/guide/agent/app-setup/console.md +8 -0
- package/guide/agent/app-setup/database-layer.md +23 -103
- package/guide/agent/app-setup/existing-application-migration.md +134 -72
- package/guide/agent/app-setup/multi-homing.md +94 -32
- package/guide/agent/app-setup/quickstart.md +1 -1
- package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
- package/guide/agent/app-setup/upgrading-jskit.md +17 -3
- package/guide/agent/app-setup/users.md +8 -5
- package/guide/agent/framework/application-operations.md +1 -1
- package/guide/agent/framework/crud-operations.md +63 -45
- package/guide/agent/framework/data-access.md +1207 -0
- package/guide/agent/framework/sanity-checks.md +26 -27
- package/guide/agent/framework/typed-features.md +117 -248
- package/guide/agent/index.md +2 -0
- package/package.json +6 -3
- package/patterns/feature-package/PATTERN.md +25 -17
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
- package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/patterns/feature-package/example/variations/types.js +1 -1
- package/patterns/minimal-foundation/example/AGENTS.md +4 -0
- package/patterns/minimal-foundation/example/package.json +4 -4
- package/patterns/minimal-foundation/example/server.js +4 -1
- package/patterns/shell-foundation/example/AGENTS.md +4 -0
- package/patterns/shell-foundation/example/package.json +5 -5
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/server.js +4 -1
- package/reference/autogen/KERNEL_MAP.md +3 -1
- package/reference/autogen/PATTERN_INDEX.md +70 -59
- package/reference/autogen/README.md +1 -1
- package/reference/autogen/packages/agent-docs.md +7 -3
- package/reference/autogen/packages/agent-runtime.md +48 -11
- package/reference/autogen/packages/architecture-inspector.md +6 -2
- package/reference/autogen/packages/assistant-core.md +4 -4
- package/reference/autogen/packages/assistant-runtime.md +33 -13
- package/reference/autogen/packages/auth-provider-local-core.md +3 -1
- package/reference/autogen/packages/auth-provider-local-db-core.md +69 -14
- package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
- package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
- package/reference/autogen/packages/connectors-core.md +22 -2
- package/reference/autogen/packages/console-core.md +9 -5
- package/reference/autogen/packages/crud-core.md +45 -175
- package/reference/autogen/packages/database-runtime-mysql.md +0 -26
- package/reference/autogen/packages/database-runtime.md +11 -33
- package/reference/autogen/packages/http-runtime.md +3 -3
- package/reference/autogen/packages/http-web.md +12 -0
- package/reference/autogen/packages/kernel.md +27 -2
- package/reference/autogen/packages/oidc-issuer-core.md +4 -0
- package/reference/autogen/packages/payments-core.md +28 -6
- package/reference/autogen/packages/realtime.md +15 -6
- package/reference/autogen/packages/resource-core.md +32 -5
- package/reference/autogen/packages/resource-crud-core.md +30 -34
- package/reference/autogen/packages/resource-runtime.md +220 -0
- package/reference/autogen/packages/rewarded-core.md +16 -10
- package/reference/autogen/packages/shell-web.md +1 -0
- package/reference/autogen/packages/users-core.md +33 -14
- package/reference/autogen/packages/workspaces-core.md +17 -10
- package/reference/autogen/tooling/config-eslint.md +74 -0
- package/reference/autogen/tooling/jskit-catalog.md +50 -1
- package/reference/autogen/tooling/jskit-release.md +5 -0
- package/skills/jskit/SKILL.md +4 -1
- package/skills/jskit/references/app-operations.md +1 -1
- package/skills/jskit/references/crud-operations.md +62 -44
- package/skills/jskit/references/data-access.md +1202 -0
- package/skills/jskit/references/database-layer.md +411 -0
- package/skills/jskit/references/existing-application-migration.md +134 -72
- package/skills/jskit/references/heuristics/INDEX.md +6 -4
- package/skills/jskit/references/heuristics/crud-authoring.md +26 -15
- package/skills/jskit/references/heuristics/data-access.md +19 -0
- package/skills/jskit/references/heuristics/filters.md +6 -6
- package/skills/jskit/references/heuristics/row-policies.md +40 -16
- package/skills/jskit/references/heuristics/server-search.md +30 -42
- package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
- package/skills/jskit/references/heuristics/typed-features.md +3 -3
- package/skills/jskit/references/multi-homing.md +94 -32
- package/skills/jskit/references/pattern-index.md +70 -59
- package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
- package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
- package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
- package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
- package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
- package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
- package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
- package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +120 -0
- package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
- package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
- package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
- package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +63 -10
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
- package/skills/jskit/references/sanity-checks.md +27 -28
- package/skills/jskit/references/typed-features.md +117 -248
- package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
- package/reference/autogen/packages/json-rest-api-core.md +0 -66
- package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
- package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
- package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
- package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
- /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
- /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
|
@@ -5,28 +5,26 @@
|
|
|
5
5
|
Use when:
|
|
6
6
|
- a CRUD list accepts `q` or other server-side query params
|
|
7
7
|
- a resource needs alias filters such as `onlyArchived -> archived`
|
|
8
|
-
- a list needs
|
|
8
|
+
- a list needs filters with compound SQL (status buckets, null/not-null semantics)
|
|
9
9
|
- the user asks where search belongs on the server in a current JSKIT app
|
|
10
10
|
|
|
11
11
|
Core rule:
|
|
12
|
-
-
|
|
12
|
+
- the server search contract lives in the table resource's `searchSchema`; a server-only entry (one with `applyFilter`) goes in the owner's `owns` entry, `{ resource, searchSchema }`
|
|
13
13
|
- route validators decide which public query keys HTTP callers may send
|
|
14
|
-
-
|
|
15
|
-
- structured list filters should use `createCrudListFilterContract(...)` so route/action validators,
|
|
14
|
+
- who may see a row is never a filter: it is the row policy ([row-policies.md](./row-policies.md)), applied by the handle on every read
|
|
15
|
+
- structured list filters should use `createCrudListFilterContract(...)` so route/action validators, the search schema, and query normalization are derived from one definition
|
|
16
16
|
|
|
17
|
-
The standard CRUD
|
|
17
|
+
The standard CRUD service lists through the table's handle:
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
format: "jsonapi"
|
|
23
|
-
}, createJsonRestContext(context));
|
|
20
|
+
// what defineCrudFeature's service does; a decorateService list maps structured filters first
|
|
21
|
+
return repository.query(buildJsonRestQueryParams("contacts", contactListFilterContract.toJsonRestQuery(query)), { ...options, format: "jsonapi" });
|
|
24
22
|
```
|
|
25
23
|
|
|
26
24
|
The normal layer split is:
|
|
27
25
|
1. `actions.js` / `registerRoutes.js`: accept and validate public query keys
|
|
28
|
-
2. `
|
|
29
|
-
3.
|
|
26
|
+
2. the feature's `decorateService`: call `listFilterContract.toJsonRestQuery(query)` for structured filters before the standard `list`; visibility belongs in the row policy, never in a filter key
|
|
27
|
+
3. the owner feature: backend filter behavior goes in the table resource's `searchSchema`, or a server-only one in its `owns` entry (`{ resource, searchSchema }`)
|
|
30
28
|
|
|
31
29
|
### Shared list and HTTP transport
|
|
32
30
|
|
|
@@ -74,13 +72,13 @@ Do not use `search: true` when:
|
|
|
74
72
|
- you want a different public key than the storage field
|
|
75
73
|
- you need one key to search several fields
|
|
76
74
|
- you need custom SQL semantics
|
|
77
|
-
- you need
|
|
75
|
+
- you need a key the service maps from a structured filter
|
|
78
76
|
|
|
79
77
|
Use explicit `searchSchema` when:
|
|
80
78
|
- the public key differs from the field name
|
|
81
79
|
- one key searches multiple fields
|
|
82
80
|
- the route/API contract should stay stable even if field names change
|
|
83
|
-
- the
|
|
81
|
+
- the key is internal and only the service's structured-filter mapping sends it
|
|
84
82
|
- you need to override the simple `search: true` behavior
|
|
85
83
|
|
|
86
84
|
Example:
|
|
@@ -110,32 +108,23 @@ Important merge rule:
|
|
|
110
108
|
Use `applyFilter` when:
|
|
111
109
|
- `actualField`, `oneOf`, and normal operators are not enough
|
|
112
110
|
- the filter needs compound SQL
|
|
113
|
-
- the filter encodes
|
|
111
|
+
- the filter encodes status buckets or null/not-null semantics that are more complex than a direct field comparison (never visibility: that is the row policy)
|
|
114
112
|
|
|
115
113
|
Example:
|
|
116
114
|
|
|
117
115
|
```js
|
|
118
116
|
searchSchema: {
|
|
119
|
-
|
|
117
|
+
dueState: {
|
|
120
118
|
type: "string",
|
|
121
119
|
applyFilter(query, rawValue) {
|
|
122
120
|
const value = String(rawValue || "").trim();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (value === "privileged") {
|
|
121
|
+
if (value === "overdue") {
|
|
122
|
+
query.whereNull("paid_at").where("due_at", "<", new Date());
|
|
126
123
|
return;
|
|
127
124
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
: "";
|
|
132
|
-
|
|
133
|
-
query.where(function () {
|
|
134
|
-
this.where("user_added", 0);
|
|
135
|
-
if (userId) {
|
|
136
|
-
this.orWhere("added_by_user_id", userId);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
125
|
+
if (value === "paid") {
|
|
126
|
+
query.whereNotNull("paid_at");
|
|
127
|
+
}
|
|
139
128
|
}
|
|
140
129
|
}
|
|
141
130
|
}
|
|
@@ -143,6 +132,7 @@ searchSchema: {
|
|
|
143
132
|
|
|
144
133
|
Keep `applyFilter` focused on query semantics:
|
|
145
134
|
- build the SQL/knex filter there
|
|
135
|
+
- read only the row's own columns in a filter a `patchWhere` or `deleteWhere` decides on: one that reads another table through a subquery is not detected, and is no compare-and-set on MySQL and MariaDB
|
|
146
136
|
- do not put permission checks there
|
|
147
137
|
- do not turn it into a second service layer
|
|
148
138
|
|
|
@@ -167,7 +157,7 @@ input: composeSchemaDefinitions([
|
|
|
167
157
|
That means:
|
|
168
158
|
- pagination, `q`, parent filters, includes, sparse fieldsets, and the
|
|
169
159
|
structured filter keys remain one standard group
|
|
170
|
-
- the
|
|
160
|
+
- the service may still map structured keys before the standard list
|
|
171
161
|
|
|
172
162
|
Use the same `listFilterQueryValidator` option with
|
|
173
163
|
`createCrudJsonApiRouteContracts(...)` at the route boundary. The layers
|
|
@@ -196,14 +186,17 @@ const receivalListFilterContract = createCrudListFilterContract(
|
|
|
196
186
|
);
|
|
197
187
|
```
|
|
198
188
|
|
|
199
|
-
Use `receivalListFilterContract.queryValidator` at route/action boundaries. Use `receivalListFilterContract.jsonRestSearchSchema`
|
|
189
|
+
Use `receivalListFilterContract.queryValidator` at route/action boundaries. Use `receivalListFilterContract.jsonRestSearchSchema` as the `searchSchema` of the table's `owns` entry. Use `receivalListFilterContract.toJsonRestQuery(query)` in the service's `list` before the standard one.
|
|
200
190
|
|
|
201
191
|
That contract preserves arrays for `enumMany` and `recordIdMany`, maps range filters to internal JSON REST search keys, and keeps those internal keys out of the public HTTP query contract.
|
|
202
192
|
|
|
203
193
|
Do not add route validators for:
|
|
204
|
-
-
|
|
194
|
+
- internal keys that only the service's structured-filter mapping sends
|
|
205
195
|
- filters that must never be supplied by the client directly
|
|
206
196
|
|
|
197
|
+
Who may see a row is never a search key at all: it is the row policy, which
|
|
198
|
+
the handle applies to every read whatever the query says.
|
|
199
|
+
|
|
207
200
|
## Recommended Shapes
|
|
208
201
|
|
|
209
202
|
Simple direct field exposure:
|
|
@@ -236,15 +229,10 @@ searchSchema: {
|
|
|
236
229
|
}
|
|
237
230
|
```
|
|
238
231
|
|
|
239
|
-
|
|
232
|
+
Visibility, by the owner, on every read:
|
|
240
233
|
|
|
241
234
|
```js
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
...(query || {}),
|
|
245
|
-
"__viewerAccess": resolveViewerAccess(context)
|
|
246
|
-
};
|
|
247
|
-
}
|
|
235
|
+
owns: [{ resource: contactResource, rowPolicy: ownContactsOrShared }]
|
|
248
236
|
```
|
|
249
237
|
|
|
250
238
|
## Keep These Boundaries Clean
|
|
@@ -252,8 +240,8 @@ function buildScopedQuery(query = {}, context = null) {
|
|
|
252
240
|
- Put backend filter behavior in `searchSchema`, not in page code.
|
|
253
241
|
- Use `createCrudListFilterContract(...)` for structured list filters before writing custom JSON REST query glue.
|
|
254
242
|
- Put permission checks in the action/service layer, not in `applyFilter`.
|
|
255
|
-
- Put
|
|
256
|
-
- Do not expose a query key publicly just because the
|
|
243
|
+
- Put visibility in the owner's row policy, never in a search key or route query validation.
|
|
244
|
+
- Do not expose a query key publicly just because the service maps to it internally.
|
|
257
245
|
- Do not rely on route validators alone; a public key still needs a matching backend search definition.
|
|
258
246
|
|
|
259
247
|
## Quick Rule Of Thumb
|
|
@@ -262,4 +250,4 @@ function buildScopedQuery(query = {}, context = null) {
|
|
|
262
250
|
- Public alias or multi-field search: `searchSchema`
|
|
263
251
|
- Complex SQL semantics: `searchSchema` + `applyFilter`
|
|
264
252
|
- Public HTTP query key: add it through the standard route/action query group
|
|
265
|
-
-
|
|
253
|
+
- Who may see a row: the owner's row policy, never a key
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!-- Generated from `packages/agent-docs/heuristics/table-resource-fields.md`. -->
|
|
2
|
+
|
|
3
|
+
# Table Resource Fields
|
|
4
|
+
|
|
5
|
+
Use when:
|
|
6
|
+
- a table resource field needs an explicit column mapping
|
|
7
|
+
- a field should appear in records but is not a real column
|
|
8
|
+
- the user mentions computed fields, projections or date write serialization
|
|
9
|
+
|
|
10
|
+
Default JSKIT pattern:
|
|
11
|
+
1. The table resource's schema describes the record; each field maps to its column itself.
|
|
12
|
+
2. Use `storage: { column: "<column>" }` (or `actualField`) only when the default snake_case column is wrong.
|
|
13
|
+
3. Standard writable `dateTime` fields are serialized at the write seam automatically; `storage.writeSerializer` is only for non-default serialization.
|
|
14
|
+
4. A computed output field is `storage: { virtual: true }` in the shared schema, and its SQL goes in the owner's `owns` entry as `queryFields`, on the server.
|
|
15
|
+
5. Handles apply those projections on every read; no repository method re-selects them.
|
|
16
|
+
6. A field's default value is `defaultTo`; `default` is refused, since json-rest-api would ignore it.
|
|
17
|
+
7. A field no request may see (an ownership id, a bearer token) is `hidden: true`, and stays hidden: the owner's code
|
|
18
|
+
reads it by naming it in a handle read's `fields`, and a system context may name it through another feature's
|
|
19
|
+
CRUD repository. Unhiding it puts it in every read's answer, other features' included; never read it with `scopedQuery`.
|
|
20
|
+
|
|
21
|
+
Field metadata rules:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// a column override
|
|
25
|
+
createdAt: { type: "dateTime", required: true, storage: { column: "created_at" } }
|
|
26
|
+
|
|
27
|
+
// a non-default write serializer
|
|
28
|
+
arrivalDatetime: { type: "dateTime", required: true, storage: { writeSerializer: "datetime-utc" } }
|
|
29
|
+
|
|
30
|
+
// a computed output field
|
|
31
|
+
remainingWeight: { type: "number", required: true, storage: { virtual: true } }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- omit `actualField`/`storage` when the field is column-backed and the default snake_case mapping is correct
|
|
35
|
+
- `storage: { virtual: true }` cannot also define `actualField`, `storage.column` or `storage.writeSerializer`
|
|
36
|
+
- `storage: { virtual: true }` fields are never written
|
|
37
|
+
|
|
38
|
+
Computed fields (server only):
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const ReceivalsFeature = defineFeature({
|
|
42
|
+
id: "receivals",
|
|
43
|
+
owns: [{
|
|
44
|
+
resource: receivalResource,
|
|
45
|
+
queryFields: {
|
|
46
|
+
remainingWeight: {
|
|
47
|
+
type: "number",
|
|
48
|
+
select({ raw, column }) {
|
|
49
|
+
return raw("?? - coalesce(??, 0)", [column("receivedWeight"), column("processedWeight")]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}],
|
|
54
|
+
setup: ({ resources }) => ({ receivals: createReceivalsRepository(resources) })
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Keep projection SQL in the server feature that owns the table, never in the shared resource that ships to the browser.
|
|
59
|
+
`select` receives `raw`, which builds one SQL expression from a string literal and its bindings (a value only: it never runs on its own),
|
|
60
|
+
`column(field)`, which names a field's column, and the read's `filters`; never Knex. The expression is the field's value
|
|
61
|
+
only, so it reads no other table.
|
|
62
|
+
|
|
63
|
+
What the resource runtime does for you:
|
|
64
|
+
- reads select only column-backed fields plus the registered query fields
|
|
65
|
+
- writes serialize standard writable `dateTime` fields, and any explicit `storage.writeSerializer`
|
|
66
|
+
- database reads serialize `date`, `time` and `dateTime` values to their strict `json-rest-schema` strings before validation, respecting `temporalPrecision`
|
|
67
|
+
- search and filters only use column-backed fields
|
|
68
|
+
|
|
69
|
+
Avoid:
|
|
70
|
+
- `clearSelect()` / re-select hacks in a repository method to add a computed field
|
|
71
|
+
- SQL fragments or joins in shared field metadata
|
|
72
|
+
- `storage` modes beyond the documented `column`, `writeSerializer` and `virtual`
|
|
73
|
+
|
|
74
|
+
Review checks:
|
|
75
|
+
- one table resource per table, and field definitions own their mapping
|
|
76
|
+
- non-default write serialization lives in `storage.writeSerializer`, not in repository code
|
|
77
|
+
- every `storage: { virtual: true }` field has a matching `queryFields` entry in the owner's `owns`
|
|
78
|
+
- no JavaScript `Date` object crosses a strict resource validation boundary; scoped-query reports return ISO/RFC 3339 strings explicitly
|
|
@@ -63,11 +63,11 @@ ends with the same link.
|
|
|
63
63
|
- [One helper per package](../typed-features.md#one-helper-per-package)
|
|
64
64
|
- `options?.trx || options?.transaction`, a hand-read actor, cursor or created id
|
|
65
65
|
- [One reader per fact](../typed-features.md#one-reader-per-fact)
|
|
66
|
-
- `
|
|
66
|
+
- a list read with `query()` where every row is meant (`queryAll()`), a list that stops at one page, `page: { size: 500 }` refused above the page limit
|
|
67
67
|
- [Every row](../typed-features.md#every-row)
|
|
68
|
-
- `await
|
|
68
|
+
- `await handle.get(…)`, `handle.query(…)` or a service `get` inside a loop or a `Promise.all` over a list, a single-record helper reused for a list, N+1 queries, a list whose query count grows with its rows, a slow list endpoint
|
|
69
69
|
- [Lists read related rows in batches](../typed-features.md#lists-read-related-rows-in-batches)
|
|
70
|
-
- a `rowPolicy` that loads a person's whole history or calls an access check per item, `
|
|
70
|
+
- a `rowPolicy` that loads a person's whole history or calls an access check per item, `create()` per row of a batch instead of `createMany()`, a write that gets slower as history grows
|
|
71
71
|
- [Bounded access checks](../typed-features.md#bounded-access-checks)
|
|
72
72
|
- a route that queries storage, `boot*.js` holding routes
|
|
73
73
|
- [Routes](../typed-features.md#routes)
|
|
@@ -110,14 +110,25 @@ The package migrations create and own these tables:
|
|
|
110
110
|
| `workspace_invites` | `workspace_id`, `email`, `role_sid`, `status`, `token_hash`, `invited_by_user_id`, `expires_at`, `accepted_at`, `revoked_at` |
|
|
111
111
|
|
|
112
112
|
Application records that belong to a workspace use a non-null `workspace_id`
|
|
113
|
-
foreign key to `workspaces.id`. Read and change these tables through
|
|
114
|
-
`workspaces.
|
|
113
|
+
foreign key to `workspaces.id`. Read and change these tables through their
|
|
114
|
+
owners' capabilities (`workspaces`, `workspaces.memberships` and
|
|
115
|
+
`workspaces.core`, below), not directly.
|
|
115
116
|
|
|
116
117
|
## Server composition
|
|
117
118
|
|
|
118
119
|
Use `workspaces/workspace-server` for role vocabulary, creation policy,
|
|
119
120
|
invitations, settings, and app-owned invitation email rendering.
|
|
120
121
|
|
|
122
|
+
Two server settings are required, and the server refuses to start without
|
|
123
|
+
them: whether new workspaces accept invitations, and how long an invitation
|
|
124
|
+
lasts.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
// config/server.js
|
|
128
|
+
config.workspaceSettings = { defaults: { invitesEnabled: true } };
|
|
129
|
+
config.workspaceMembers = { defaults: { inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000 } };
|
|
130
|
+
```
|
|
131
|
+
|
|
121
132
|
Workspace scope must reach repository queries. Parsing a workspace slug or
|
|
122
133
|
hiding an admin link is not authorization. Membership and role checks happen
|
|
123
134
|
before an operation discloses records or record existence.
|
|
@@ -125,14 +136,12 @@ before an operation discloses records or record existence.
|
|
|
125
136
|
### Workspace-scoped features and routes
|
|
126
137
|
|
|
127
138
|
For a standard CRUD resource owned by a workspace, give its table the
|
|
128
|
-
`workspace_id` column, set
|
|
129
|
-
`
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`), which throws when the
|
|
135
|
-
context names two workspaces.
|
|
139
|
+
`workspace_id` column, set its table resource's `ownership: "workspace"`, and bind
|
|
140
|
+
`defineCrudFeature()` with `scope: WORKSPACE_CRUD_SCOPE` (from
|
|
141
|
+
`@jskit-ai/workspaces-core/server/support/workspaceCrud`) on a workspace
|
|
142
|
+
surface. JSKIT then filters `workspaceId` by the workspace in the route. A
|
|
143
|
+
repository method you add passes the call's `options` on to the table's handle,
|
|
144
|
+
which scopes every read and write to the context's workspace.
|
|
136
145
|
`workspace_user` also filters by the signed-in user. See the CRUD operations
|
|
137
146
|
guide.
|
|
138
147
|
|
|
@@ -175,9 +184,29 @@ capability provides:
|
|
|
175
184
|
| `workspaces.services.members` | Members, roles and invitations |
|
|
176
185
|
| `workspaces.services.pendingInvitations` | Invitation resolution, redemption and acceptance participants |
|
|
177
186
|
| `workspaces.services.settings` | Workspace settings |
|
|
178
|
-
| `workspaces.repositories.
|
|
187
|
+
| `workspaces.repositories.workspaceSettings`, `.workspaceInvites` | Settings and invitation records inside the application's own transactions |
|
|
179
188
|
| `workspaces.tenancyProfile` | The resolved tenancy mode and workspace policy |
|
|
180
189
|
|
|
190
|
+
The workspaces and their memberships are their own capabilities, for code that
|
|
191
|
+
reads or changes them inside its own transactions, typed `Workspaces` and
|
|
192
|
+
`WorkspacesMemberships` in `@jskit-ai/workspaces-core/server/types`. Every
|
|
193
|
+
method takes the call's `options` last and joins the transaction of the
|
|
194
|
+
`txOptions` it is given.
|
|
195
|
+
|
|
196
|
+
| Capability | Methods |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| `workspaces` | `findById(id, options)`, `findBySlug(slug, options)` (a deleted workspace is not found, though its slug stays taken), `listSlugsIn`, `findPersonalByOwnerUserId`, `listForUserId(userId, options)`, `insert`, `updateById`, `lock(workspaceId, options)` |
|
|
199
|
+
| `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds` |
|
|
200
|
+
|
|
201
|
+
`addMember` makes the user an active member with the role (`member` when none
|
|
202
|
+
is given), adding the membership or reactivating it. `activeMemberIds` answers
|
|
203
|
+
the active members' user ids at the same cost however many members there
|
|
204
|
+
are, without including their profiles. `lock` takes
|
|
205
|
+
the workspace's row lock until the caller's transaction ends, so work that must
|
|
206
|
+
not interleave for one workspace takes turns; it runs only inside a
|
|
207
|
+
transaction, given its `txOptions`, and answers `{ id }`, or `null` when there
|
|
208
|
+
is no such workspace.
|
|
209
|
+
|
|
181
210
|
To check one person's membership, for example a notification recipient, call
|
|
182
211
|
`workspaces.services.members.getMember(workspace, userId, options)`. It answers
|
|
183
212
|
that member's `listMembers` entry, or `null` for a removed or unknown member,
|
|
@@ -225,8 +254,8 @@ workspace routes, `workspaces-web` switches Vuetify to the `workspace-light` or
|
|
|
225
254
|
colours. Other routes use the application's themes.
|
|
226
255
|
|
|
227
256
|
New workspaces start from JSKIT's blue palette unless `config/server.js` sets
|
|
228
|
-
the application's own
|
|
229
|
-
stops the server at startup:
|
|
257
|
+
the application's own beside the required `invitesEnabled`. Missing colours
|
|
258
|
+
keep JSKIT's values; an invalid colour stops the server at startup:
|
|
230
259
|
|
|
231
260
|
```js
|
|
232
261
|
// config/server.js
|
|
@@ -301,11 +330,11 @@ authorization and role checks. Do not pass arbitrary HTTP input as options.
|
|
|
301
330
|
|
|
302
331
|
| Capability method | Contract |
|
|
303
332
|
| --- | --- |
|
|
304
|
-
| `workspaces.repositories.workspaceInvites.withTransaction(
|
|
305
|
-
| `workspaces.
|
|
306
|
-
| `workspaces.services.members.prepareInvite(workspace, actor, payload,
|
|
307
|
-
| `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` |
|
|
308
|
-
| `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it.
|
|
333
|
+
| `workspaces.repositories.workspaceInvites.withTransaction(options, (txOptions) => …)` | The invitations handle's `withTransaction`: joins the caller's transaction or opens one and hands `work` the `txOptions` to pass on; awaits commit and completion hooks before resolving. Runs `work` again when the database aborts it as a deadlock victim (see [data access](https://mobily-enterprises.github.io/jskit-ai/guide/framework/data-access#transactions)). `{ ...options, outermost: true }` refuses a call that already runs inside one. |
|
|
334
|
+
| `workspaces.lock(workspaceId, txOptions)` (capability `workspaces`) | Locks the workspace before application receipt/draft rows when composing preparation. Requires an active transaction. |
|
|
335
|
+
| `workspaces.services.members.prepareInvite(workspace, actor, payload, txOptions)` | Runs only inside a transaction, given its `txOptions`. Validates the invitation role, replaces pending invitations for that workspace/email, and returns `{ createdInviteId, inviteTokenPreview }`. Never sends. |
|
|
336
|
+
| `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` | Refuses to run inside a transaction: it delivers after the preparation's transaction commits. Reloads the committed pending invitation and workspace display data, validates expiry and token correspondence, and sends that invitation without replacement. |
|
|
337
|
+
| `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it. Refuses to run inside a transaction. |
|
|
309
338
|
| `workspaces.invite-acceptance-participant` (optional capability) | One application function receiving `({ invite, user }, options)` inside the acceptance transaction. Without it, acceptance activates the membership only. |
|
|
310
339
|
|
|
311
340
|
`sendInvite` accepts `{ createdInviteId, inviteTokenPreview }` from preparation or
|
|
@@ -344,21 +373,20 @@ setup: ({ reviewedPeople, people, scopes, audit }) => ({
|
|
|
344
373
|
})
|
|
345
374
|
|
|
346
375
|
// Inside an authorized application command, in a feature that requires
|
|
347
|
-
// workspaces: "workspaces.core":
|
|
376
|
+
// workspaces: "workspaces.core" and workspaceRecords: "workspaces":
|
|
348
377
|
const { members } = workspaces.services;
|
|
349
378
|
const invites = workspaces.repositories.workspaceInvites;
|
|
350
|
-
const prepared = await invites.withTransaction(async (
|
|
351
|
-
const options = { trx, context };
|
|
379
|
+
const prepared = await invites.withTransaction({ context }, async (txOptions) => {
|
|
352
380
|
// Match acceptance's order: workspace, then application draft/receipt rows.
|
|
353
|
-
await
|
|
354
|
-
const reviewed = await reviewedPeople.lockAuthorizedDraft(draftId, workspace.id, actor,
|
|
355
|
-
const receipt = await reviewedPeople.findMatchingReceipt(reviewed, requestId,
|
|
381
|
+
await workspaceRecords.lock(workspace.id, txOptions);
|
|
382
|
+
const reviewed = await reviewedPeople.lockAuthorizedDraft(draftId, workspace.id, actor, txOptions);
|
|
383
|
+
const receipt = await reviewedPeople.findMatchingReceipt(reviewed, requestId, txOptions);
|
|
356
384
|
if (receipt) return { createdInviteId: receipt.inviteId };
|
|
357
385
|
const created = await members.prepareInvite(workspace, actor, {
|
|
358
386
|
email: reviewed.email,
|
|
359
387
|
roleSid: reviewed.roleSid
|
|
360
|
-
},
|
|
361
|
-
await reviewedPeople.associateInvitationAndSaveReceipt(reviewed, requestId, created.createdInviteId,
|
|
388
|
+
}, txOptions);
|
|
389
|
+
await reviewedPeople.associateInvitationAndSaveReceipt(reviewed, requestId, created.createdInviteId, txOptions);
|
|
362
390
|
return { createdInviteId: created.createdInviteId };
|
|
363
391
|
});
|
|
364
392
|
|
|
@@ -401,8 +429,8 @@ exactly once. Revocation after sending starts cannot recall a message.
|
|
|
401
429
|
Register one callback during feature setup to call the required application
|
|
402
430
|
services. Non-functions throw `TypeError`; duplicate registration throws, and
|
|
403
431
|
registration closes when acceptance/refusal first begins. The callback's invite,
|
|
404
|
-
user and options are shallow frozen. JSKIT owns the transaction and passes
|
|
405
|
-
`
|
|
432
|
+
user and options are shallow frozen. JSKIT owns the transaction and passes the
|
|
433
|
+
callback the `txOptions` that carry it and the request's `context`.
|
|
406
434
|
|
|
407
435
|
Acceptance checks authentication, recipient email, pending state and expiry under
|
|
408
436
|
workspace/invitation locks. Before calling the participant, it activates membership
|
|
@@ -420,8 +448,8 @@ callback must not commit, roll back, create transactions/savepoints or perform
|
|
|
420
448
|
external effects. Participant and later invitation-status failures roll back
|
|
421
449
|
membership (including its previous role/status), person/training links, scopes,
|
|
422
450
|
audit and accepted status. Invalid invitations and refusals do not call the
|
|
423
|
-
participant. Acceptance
|
|
424
|
-
|
|
451
|
+
participant. Acceptance and refusal refuse to run inside the caller's
|
|
452
|
+
transaction; revocation can join one or open its own.
|
|
425
453
|
|
|
426
454
|
Acceptance/refusal return 401 for unauthenticated access, 403 for a recipient
|
|
427
455
|
email mismatch and 404 for a missing or handled invitation. A matching recipient's
|
|
@@ -435,7 +463,7 @@ Preserve `error.transactionOutcome` and `error.cause`: `rolledBack` confirms
|
|
|
435
463
|
rollback, `committed` can accompany a failed completion hook, and `unknown`
|
|
436
464
|
requires resolving the outcome before retrying. Errors after commit cannot undo
|
|
437
465
|
persistence. See
|
|
438
|
-
[managed transaction guidance](existing-application-migration.md#json-rest-
|
|
466
|
+
[managed transaction guidance](existing-application-migration.md#data-access-on-table-resources-json-rest-api-3).
|
|
439
467
|
|
|
440
468
|
Invitation mutations lock workspace before invitation rows. A losing acceptance,
|
|
441
469
|
refusal, revocation or replacement cannot overwrite accepted/revoked state.
|
|
@@ -494,7 +522,41 @@ Workspace surfaces put the slug in their pages root, such as
|
|
|
494
522
|
`w/[workspaceSlug]/admin` for administration. Keep the default surface
|
|
495
523
|
(`surfaceDefaultId`, usually `home`) free of a workspace requirement: `/`,
|
|
496
524
|
invitation links and other unscoped pages belong to it. With
|
|
497
|
-
`webRootAllowed: "no"`, `/` opens that surface's root.
|
|
525
|
+
`webRootAllowed: "no"`, `/` opens that surface's root. The foundation
|
|
526
|
+
defines `home` and its `public` access policy, and the `auth/auth-surface`
|
|
527
|
+
pattern the `auth` surface; the workspace surfaces add a policy and two
|
|
528
|
+
definitions:
|
|
529
|
+
|
|
530
|
+
```js
|
|
531
|
+
// config/surfaceAccessPolicies.js
|
|
532
|
+
surfaceAccessPolicies.workspace_member = { requireAuth: true, requireWorkspaceMembership: true };
|
|
533
|
+
|
|
534
|
+
// config/public.js
|
|
535
|
+
config.surfaceDefinitions.app = {
|
|
536
|
+
id: "app",
|
|
537
|
+
label: "App",
|
|
538
|
+
pagesRoot: "w/[workspaceSlug]",
|
|
539
|
+
requiresAuth: true,
|
|
540
|
+
requiresWorkspace: true,
|
|
541
|
+
accessPolicyId: "workspace_member"
|
|
542
|
+
};
|
|
543
|
+
config.surfaceDefinitions.admin = {
|
|
544
|
+
id: "admin",
|
|
545
|
+
label: "Admin",
|
|
546
|
+
pagesRoot: "w/[workspaceSlug]/admin",
|
|
547
|
+
requiresAuth: true,
|
|
548
|
+
requiresWorkspace: true,
|
|
549
|
+
accessPolicyId: "workspace_member"
|
|
550
|
+
};
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
An access policy takes `requireAuth`, `requireWorkspaceMembership`,
|
|
554
|
+
`requireAnyPermissions` and `requireAllPermissions` (permission lists), and
|
|
555
|
+
`requireFlagsAny` and `requireFlagsAll` (flags the server's bootstrap sets in
|
|
556
|
+
`surfaceAccess`). Without `requireAuth` or `requireWorkspaceMembership` it
|
|
557
|
+
takes the surface's `requiresAuth` or `requiresWorkspace`. The browser applies
|
|
558
|
+
the whole policy; the server reads only `requireWorkspaceMembership: false`
|
|
559
|
+
from it, and a route's own `auth` and `permission` authorize it there.
|
|
498
560
|
|
|
499
561
|
`workspaces-web` remembers the last workspace each person opened in this
|
|
500
562
|
browser. The **Your workspaces** page, `WorkspacesClientElement` from
|