@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
|
@@ -96,31 +96,33 @@ application's normal verification script, and Playwright.
|
|
|
96
96
|
|
|
97
97
|
## 3. Choose the narrowest server seam that owns each behavior
|
|
98
98
|
|
|
99
|
-
Conventional resources use `
|
|
100
|
-
`
|
|
101
|
-
permission, action, JSON API, and route mechanics. Extend that contract instead
|
|
99
|
+
Conventional resources use `defineTableResource()`, `defineCrudContract()`
|
|
100
|
+
and `defineCrudFeature()` so JSKIT owns the repeated repository,
|
|
101
|
+
service, permission, action, JSON API, and route mechanics. Extend that contract instead
|
|
102
102
|
of copying it.
|
|
103
103
|
|
|
104
104
|
| Product need | Normal owner |
|
|
105
105
|
| --- | --- |
|
|
106
106
|
| Behavior before, around, or after one standard CRUD operation | `operationLifecycle` |
|
|
107
107
|
| A resource-specific domain method or override | `decorateService` |
|
|
108
|
-
| A resource-specific query, lock, or
|
|
108
|
+
| A resource-specific query, lock, or many-row write | `decorateRepository`, on the table's handle |
|
|
109
109
|
| A named non-CRUD command on the resource | a named CRUD `action` backed by the service |
|
|
110
110
|
| A separate aggregate, workflow, or capability domain | `defineFeature()` with named actions |
|
|
111
|
-
|
|
|
111
|
+
| A report the handle cannot express | one `scopedQuery()` repository method, under `// Knex: <kind> - <reason>` ([data access](data-access.md#layer-5-reports-the-resource-cannot-express)) |
|
|
112
112
|
|
|
113
113
|
For mutations, lifecycle phases before commit share one repository
|
|
114
114
|
transaction. An `execute` hook that only normalizes input calls
|
|
115
|
-
`standard(nextInput)` rather than reimplementing the write.
|
|
116
|
-
|
|
115
|
+
`standard(nextInput)` rather than reimplementing the write. The feature that
|
|
116
|
+
owns a table reads and writes it through the handles its `owns` entry gives
|
|
117
|
+
its repository; services, hooks and other features call that repository or
|
|
118
|
+
the owner's API, never the table. External work
|
|
117
119
|
runs after commit, or a durable outbox record is written inside the transaction
|
|
118
120
|
and delivered separately.
|
|
119
121
|
|
|
120
122
|
Give commands product names such as `confirm`, `publish`, or `cancel`, with
|
|
121
123
|
explicit input, permissions, routes where needed, audit behavior, events, and
|
|
122
124
|
tests. Do not hide unrelated workflows behind a generic `execute(anything)`
|
|
123
|
-
action, and do not
|
|
125
|
+
action, and do not write Knex merely because SQL is familiar.
|
|
124
126
|
|
|
125
127
|
### Reuse response serializers
|
|
126
128
|
|
|
@@ -143,9 +145,11 @@ upgrading JSKIT and declaring the directly used
|
|
|
143
145
|
```js
|
|
144
146
|
import Fastify from "fastify";
|
|
145
147
|
import SerializerSelector from "@fastify/fast-json-stringify-compiler";
|
|
146
|
-
import { createCachedResponseSerializerFactory } from "@jskit-ai/kernel/server/http";
|
|
148
|
+
import { createCachedResponseSerializerFactory, resolveTrustedProxies } from "@jskit-ai/kernel/server/http";
|
|
147
149
|
|
|
148
150
|
const app = Fastify({
|
|
151
|
+
// TRUSTED_PROXIES: the proxies whose X-Forwarded-For is believed (loopback without it).
|
|
152
|
+
trustProxy: resolveTrustedProxies(runtimeEnv),
|
|
149
153
|
schemaController: {
|
|
150
154
|
compilersFactory: {
|
|
151
155
|
buildSerializer: createCachedResponseSerializerFactory(SerializerSelector())
|
|
@@ -155,84 +159,142 @@ const app = Fastify({
|
|
|
155
159
|
```
|
|
156
160
|
|
|
157
161
|
Keep the application's other Fastify options, including serializer options and
|
|
158
|
-
validator configuration
|
|
162
|
+
validator configuration, but replace an application's own `trustProxy` setting
|
|
163
|
+
with `resolveTrustedProxies(runtimeEnv)` (`runtimeEnv` is the environment the
|
|
164
|
+
server starts with, `process.env` if it has no other), and its environment
|
|
165
|
+
variable with `TRUSTED_PROXIES`; see
|
|
166
|
+
[sign-in protection](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/authentication#sign-in-protection)
|
|
167
|
+
for the setting. This setup wraps the native default serializer builder;
|
|
159
168
|
retain any deliberately chosen custom compiler instead of replacing or wrapping
|
|
160
169
|
it. Native route-level compiler overrides continue to apply. Update the lockfile
|
|
161
170
|
through the application's normal installation flow and restart the backend to
|
|
162
171
|
activate the new setup. Updating packages or starter examples alone does not
|
|
163
172
|
change an existing application's startup source.
|
|
164
173
|
|
|
165
|
-
###
|
|
174
|
+
### Data access on table resources (json-rest-api 3)
|
|
166
175
|
|
|
167
|
-
Applications using `@jskit-ai/
|
|
176
|
+
Applications using `@jskit-ai/resource-runtime` require Node.js 24 or newer.
|
|
168
177
|
JSKIT's repository and application default is Node.js 26. Upgrade dependencies,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
The standard `defineCrudJsonApiFeature()` API keeps `queryDocuments`,
|
|
173
|
-
`getDocumentById`, `createDocument`, `patchDocumentById`, and
|
|
174
|
-
`deleteDocumentById`, with `{ trx, context }` repository/service options.
|
|
175
|
-
Read, create, and patch services still return tagged JSON:API documents.
|
|
176
|
-
Application-owned hosts use `JsonRestApi` from `json-rest-api`; the JSKIT host
|
|
177
|
-
already owns that setup.
|
|
178
|
-
|
|
179
|
-
For direct resource calls, replace `inputRecord` with exactly one of `data`
|
|
180
|
-
(plain values) or `document` (a JSON:API document). `format` selects output
|
|
181
|
-
independently: `"plain"` or `"jsonapi"`. Replace `simplified` and
|
|
182
|
-
`returnFullRecord` with `format` and `returning: "full" | "minimal" | "none"`.
|
|
183
|
-
The JSKIT host defaults to plain output and full write responses; standard
|
|
184
|
-
document repositories explicitly request JSON:API output and full create/patch
|
|
185
|
-
responses. A plain collection is still `{ data: [...], meta?, links? }`; use
|
|
186
|
-
`extractJsonRestCollectionRows()` when a custom repository needs its rows.
|
|
187
|
-
A query without a page returns only the resource's first 20 rows; a
|
|
188
|
-
server-side read that needs every row uses `queryAllJsonRestCollectionRows()`
|
|
189
|
-
([every row](typed-features.md#every-row)).
|
|
178
|
+
resource calls and hooks together; rebuild native database dependencies after
|
|
179
|
+
changing Node.
|
|
190
180
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
data: { title: "Revised title" },
|
|
195
|
-
format: "jsonapi",
|
|
196
|
-
returning: "full",
|
|
197
|
-
transaction: trx
|
|
198
|
-
}, createJsonRestContext(context));
|
|
199
|
-
```
|
|
181
|
+
Every table has one table resource and one owner feature, and every read and
|
|
182
|
+
write goes through that table's handle
|
|
183
|
+
([data access](data-access.md)). Rename as you go:
|
|
200
184
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
`
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
`
|
|
220
|
-
|
|
221
|
-
`
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
185
|
+
| Before | Now |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `defineCrudResource()` | `defineTableResource()` plus `defineCrudContract()` |
|
|
188
|
+
| `defineCrudJsonApiFeature()`, `createCrudResourceRuntime()` | `defineCrudFeature({ crud })` |
|
|
189
|
+
| the CRUD runtime's `listByIds()`, `listByForeignIds()` | `queryAll({ filters: { [key]: ids } })` on a filter declared `{ type: "array", filterOperator: "in" }`; a page's children with `createHooksToCollectChildren()` |
|
|
190
|
+
| the CRUD runtime's lookup hydration | `include` on the CRUD list or view |
|
|
191
|
+
| the CRUD runtime's `virtualFields` | `queryFields` in the `owns` entry, or `decorateService` for a value SQL cannot compute |
|
|
192
|
+
| the CRUD runtime's `createdAt`/`updatedAt` stamping | every handle write stamps them; only a system context sets its own |
|
|
193
|
+
| the CRUD runtime's output validation | the table resource's schema, whose types json-rest-api reads each field as |
|
|
194
|
+
| `defineCrudFeature`'s `ownershipFilter`, `searchSchema` | `visibility`; an `owns` entry |
|
|
195
|
+
| a lifecycle hook's `trx`; a decorator's or CRUD capability's `resource` | `options`; `crud` |
|
|
196
|
+
| `decorateRepository`'s `database`, `http`, `jsonRestApi` | its `repository` and `resources`, each owned table's handle |
|
|
197
|
+
| `@jskit-ai/crud-core/server/fieldAccess`, `/server/lookups` | removed (nothing called them); restrict fields in the CRUD contract and its `decorateService` |
|
|
198
|
+
| `runtime.json-rest-api`, `addResourceIfMissing()`, `createJsonRestResourceScopeOptions()` | `defineFeature({ owns })`; a row policy, search schema or query fields go in the `owns` entry |
|
|
199
|
+
| `queryDocuments()`, `getDocumentById()` | `query({ page: { size } })`, `get(id)` |
|
|
200
|
+
| `queryAllJsonRestCollectionRows()`, `queryEveryDocument()` | `queryAll(params)` |
|
|
201
|
+
| `createDocument()`, `createDocuments()` | `create()`, `createMany()` |
|
|
202
|
+
| `patchDocumentById()`, `patchDocumentIfLockVersion()` | `patch(id, changes, options)`, `patch(id, changes, { ...options, lockVersion })` |
|
|
203
|
+
| `deleteDocumentById()` | `delete(id)` |
|
|
204
|
+
| `getDocumentById(…, { forUpdate: true })`, `applyForUpdate()`, `lockScopedRecordId()` | `lock(id)` inside `withTransaction` |
|
|
205
|
+
| `runInTransaction()`, `createWithTransaction()`, `joinOrOpenTransaction()`, `resolveTransaction()`, `resolveQueryOptions()`, `resolveRepoClient()` | `withTransaction(options, (txOptions) => …)` |
|
|
206
|
+
| `returning: "full" \| "minimal"` | `returning: "record" \| "id"` (`"none"` is unchanged) |
|
|
207
|
+
| a hand-built `visibilityContext` or scope values, `applyVisibility()`, `applyVisibilityOwners()` | the caller's context, or `systemContext(reason).forWorkspace(id)` |
|
|
208
|
+
| a field's `default:` | `defaultTo:` (`default` is refused, since json-rest-api never read it) |
|
|
209
|
+
| a `createdAt` or `updatedAt` default of now | nothing: drop it, since every handle write stamps them |
|
|
210
|
+
| a CRUD contract that showed every column | a contract shows only its `fields`, so list every field the API showed |
|
|
211
|
+
| reading every row | `queryAll`, which refuses above 10,000 rows, so page with `query()` |
|
|
212
|
+
| an app's own wrappers around old calls | the handle method they wrapped, then delete the wrapper |
|
|
213
|
+
|
|
214
|
+
Handles answer records, `{ id, ...fields }`, with a belongs-to field as the
|
|
215
|
+
related id; the CRUD service alone answers JSON:API documents. Every call takes
|
|
216
|
+
`options` last: the caller's context, and inside a transaction the `txOptions`
|
|
217
|
+
`withTransaction` handed its work. A call without a context throws on a table
|
|
218
|
+
that is not public.
|
|
219
|
+
|
|
220
|
+
json-rest-api 3 changes what a failed write means: any failed write inside a
|
|
221
|
+
managed transaction fails the whole transaction. "Insert, catch the duplicate,
|
|
222
|
+
then read" inside a transaction no longer works; use
|
|
223
|
+
`createMany(rows, { ...options, onConflict: { keys, action: "ignore" } })` and then read,
|
|
224
|
+
or read first. A `get`, `patch`, `delete` or `lock` of a missing or hidden
|
|
225
|
+
record throws json-rest-api's not-found error (a 404); with `missing: "skip"`
|
|
226
|
+
it answers `null`, without failing the caller's transaction. Upsert rows carry no
|
|
227
|
+
`id`, never repeat their conflict keys, and name the ownership field among
|
|
228
|
+
those keys; only a table with no owners and no row policy may key the upsert on
|
|
229
|
+
the id alone, `onConflict: { keys: ["id"] }`, with an id on every row. `patchWhere` and `deleteWhere` need filters, or `all: true`.
|
|
226
230
|
|
|
227
231
|
Preserve `error.transactionOutcome` and its cause when translating failures:
|
|
228
232
|
`none` means no transaction was acquired or accepted, `pending` leaves
|
|
229
233
|
completion to the owner, `rolledBack` confirms rollback, `committed` confirms
|
|
230
234
|
commit, and `unknown` means the outcome is unresolved. These describe database
|
|
231
|
-
transaction evidence, not external effects. A failed
|
|
232
|
-
`
|
|
233
|
-
not
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
transaction evidence, not external effects. A failed `afterCommit` hook raises
|
|
236
|
+
`RestApiWriteError` with outcome `committed` and does not undo the write. Do
|
|
237
|
+
not turn pending, committed, or unknown write failures into successful
|
|
238
|
+
missing-record results, or automatically replay a committed or unresolved
|
|
239
|
+
write.
|
|
240
|
+
|
|
241
|
+
### Moving hand-written SQL behind resources
|
|
242
|
+
|
|
243
|
+
An application with Knex in its repositories moves it behind table resources
|
|
244
|
+
one table at a time:
|
|
245
|
+
|
|
246
|
+
1. **Inventory.** List every statement that reads or writes a table: the
|
|
247
|
+
repository methods, raw queries, migrations' seed data, jobs and scripts.
|
|
248
|
+
`jskit check` and the data-access lint rules (`jskit/no-direct-knex`,
|
|
249
|
+
`jskit/no-hand-scope`) find most of them.
|
|
250
|
+
2. **One owner per table.** Pick the feature that owns each table and give the
|
|
251
|
+
table one table resource with its `ownership`. Two features writing one
|
|
252
|
+
table means one of them asks the other.
|
|
253
|
+
3. **Owner APIs.** Code outside the owner calls the owner's service or
|
|
254
|
+
repository capability. JSKIT's own tables already work this way: users
|
|
255
|
+
(`users.profiles`, `users.settings`), workspaces, local sign-in and console
|
|
256
|
+
settings.
|
|
257
|
+
4. **Map each statement to a layer**
|
|
258
|
+
([data access](data-access.md)): one record or page →
|
|
259
|
+
Layer 1; screens over HTTP → Layer 2; a loop of writes → a many-row write;
|
|
260
|
+
a `forUpdate` read → a lock; totals and reports → `aggregate`, a window
|
|
261
|
+
include, or one `scopedQuery()` method; work over every workspace → a job
|
|
262
|
+
with `systemScope`.
|
|
263
|
+
5. **Two tests per path.** `expectScoped` proves the path never reads or
|
|
264
|
+
changes a second owner's rows; `expectFlatQueries` proves its statements do
|
|
265
|
+
not grow with its rows. Both come from `@jskit-ai/resource-runtime/test`.
|
|
266
|
+
|
|
267
|
+
Keep each move behaviour-preserving
|
|
268
|
+
([refactors](sanity-checks.md#refactors)).
|
|
269
|
+
|
|
270
|
+
A table keyed by several columns, or by its owner's id, gets an integer `id`
|
|
271
|
+
first ([Layer 1](data-access.md#layer-1-the-table-resource)).
|
|
272
|
+
One new migration does it on MySQL, MariaDB and PostgreSQL: the key becomes a
|
|
273
|
+
unique index before the primary key goes, so a foreign key on its first column
|
|
274
|
+
keeps an index, and adding the `id` numbers the existing rows. `down` undoes it
|
|
275
|
+
in reverse.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
const KEY = ["workspace_id", "day"];
|
|
279
|
+
|
|
280
|
+
export async function up(knex) {
|
|
281
|
+
await knex.schema.alterTable("day_slots", (table) => table.unique(KEY, { indexName: "day_slots_key" }));
|
|
282
|
+
await knex.schema.alterTable("day_slots", (table) => table.dropPrimary());
|
|
283
|
+
await knex.schema.alterTable("day_slots", (table) => table.increments("id"));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function down(knex) {
|
|
287
|
+
await knex.schema.alterTable("day_slots", (table) => table.dropColumn("id"));
|
|
288
|
+
await knex.schema.alterTable("day_slots", (table) => table.primary(KEY));
|
|
289
|
+
await knex.schema.alterTable("day_slots", (table) => table.dropUnique(KEY, "day_slots_key"));
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
MySQL and MariaDB commit each statement on its own, so a run that fails
|
|
294
|
+
part-way resumes as the [partial-failure
|
|
295
|
+
rules](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#directory-order-comes-before-filenames)
|
|
296
|
+
say. Code that wrote by the old key now upserts on it, with the ownership
|
|
297
|
+
column among the keys.
|
|
236
298
|
|
|
237
299
|
### Workspace invitation transaction compatibility (workspaces-core 0.1.198)
|
|
238
300
|
|
|
@@ -22,7 +22,7 @@ How to use it:
|
|
|
22
22
|
- [page-scaffolding.md](./page-scaffolding.md)
|
|
23
23
|
- child crud, nested crud, embedded list, subroute, separate page, parent/child layout
|
|
24
24
|
- [child-cruds.md](./child-cruds.md)
|
|
25
|
-
- crud, crud server, crud ui, resource contract, table creation, migrations, ownership, JSON API, standard screens
|
|
25
|
+
- crud, crud server, crud ui, table resource, CRUD contract, table creation, migrations, ownership, JSON API, standard screens
|
|
26
26
|
- [crud-authoring.md](./crud-authoring.md)
|
|
27
27
|
- CRUD links, record placeholders, `paths.page()`, `resolveViewUrl`, `resolveEditUrl`, `resolveParams`
|
|
28
28
|
- [crud-links.md](./crud-links.md)
|
|
@@ -40,11 +40,13 @@ How to use it:
|
|
|
40
40
|
- [filters.md](./filters.md)
|
|
41
41
|
- `searchSchema`, `search: true`, `applyFilter`, server search, query validators, backend filters, internal JSON REST filters
|
|
42
42
|
- [server-search.md](./server-search.md)
|
|
43
|
-
- `actualField`, `storage.writeSerializer`, `storage.virtual`, computed field, virtual field, projection, `
|
|
44
|
-
- [
|
|
43
|
+
- `actualField`, `storage.column`, `storage.writeSerializer`, `storage.virtual`, `queryFields`, computed field, virtual field, projection, `defaultTo`, datetime write serialization, temporalPrecision, json-rest-schema dates
|
|
44
|
+
- [table-resource-fields.md](./table-resource-fields.md)
|
|
45
|
+
- data access, database table, knex, raw, SQL, repository, bulk, batch, many rows, set write, upsert, lock, claim, totals, report, aggregate, newest per group, transaction, `trx`, background job, system scope, `systemContext`, owner, owns, owner API, another feature's table, scope test, `expectFlatQueries`, `requireTestDatabase`, `jskit/no-direct-knex`, `jskit/no-transaction-reader`, `jskit/no-hand-scope`, `jskit/sql-literal`, `jskit/knex-reason`, `infrastructure:`
|
|
46
|
+
- [data-access.md](./data-access.md)
|
|
45
47
|
- row policy, permission filtering, visibility before pagination, recursive CTE, descendants, package cycle, `RowPolicyPlugin`, row policy cost, slow writes through a policy, access check per history item
|
|
46
48
|
- [row-policies.md](./row-policies.md)
|
|
47
|
-
- typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `
|
|
49
|
+
- typed features, server feature, provider, service, repository, collaborator, `typeof`, `= null`, `setProvider`, `register`, `input || {}`, `payload = {}`, `async execute`, `Object.freeze`, `requires`, `optional`, `types.js`, `RuntimeKnex`, `silentLogger`, `create<Name>Actions`, `ActionSpecifications`, request context, `handle.query`, `queryAll`, every row, list stops at one page, `jskit/*` lint findings, `readable-check: allow`, `optional operation:`, `process.env`, `AppError` code, `Object.assign(new Error`, `idempotency`, `…ForTests` export, file or function size, read per row, N+1 queries, slow endpoint, query budget, realistic row counts, slow tests, raised timeout, `migrate.latest()` in tests, test database snapshot, `--test-concurrency=1`, `node --test`, `jskit test`, `testConcurrency()`, `V64_TEST_CONCURRENCY`, Playwright `workers`, Vitest `maxWorkers`, tests one file at a time, test files colliding in parallel, `jskit-test: alone`, `test/*` findings of `jskit check`
|
|
48
50
|
- [typed-features.md](./typed-features.md)
|
|
49
51
|
- client features, composable, `use<Name>`, Vue component, client provider, props, emits, `inject`, `provide`, `ref`, `reactive`, `watch`, `addEventListener`, `setInterval`, module state, `Object.freeze` on a composable, `?.()` in a template, `jskit/typed-inject`, `jskit/collaborator-props`, `jskit/fixed-result-shape`, `jskit/released-listeners`, `jskit/no-module-state`, `jskit/no-frozen-vue-state`, `clientFeaturesConfig`, `shared resource:`
|
|
50
52
|
- [client-features.md](./client-features.md)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# CRUD authoring
|
|
4
4
|
|
|
5
|
-
Use this file for normal persisted entities,
|
|
5
|
+
Use this file for normal persisted entities, table resources and CRUD contracts, migrations,
|
|
6
6
|
JSON API routes, and matching screens.
|
|
7
7
|
|
|
8
8
|
## Start from product decisions
|
|
@@ -13,15 +13,18 @@ choice is missing. Do not turn these decisions into generator options.
|
|
|
13
13
|
|
|
14
14
|
Read these package-owned source patterns:
|
|
15
15
|
|
|
16
|
-
- `
|
|
17
|
-
- `crud/
|
|
16
|
+
- `database/table-resource` for the table's fields, ownership and lookups
|
|
17
|
+
- `crud/crud-contract` for the HTTP surface on it
|
|
18
|
+
- `crud/crud-feature-package` for migration and server composition
|
|
18
19
|
- `crud/crud-screen-set` for routed list, view, create, edit, and delete UI
|
|
19
20
|
|
|
20
21
|
## Normal implementation order
|
|
21
22
|
|
|
22
23
|
1. Author a source-controlled migration in the package that owns the entity.
|
|
23
|
-
2. Define the resource with `
|
|
24
|
-
|
|
24
|
+
2. Define the table resource with `defineTableResource()` and its HTTP surface
|
|
25
|
+
with `defineCrudContract()`.
|
|
26
|
+
3. Bind standard server behavior with `defineCrudFeature()`, which owns the
|
|
27
|
+
table and serves its CRUD contract.
|
|
25
28
|
4. Add only product-specific service methods, lifecycle, actions, validation,
|
|
26
29
|
policy, queries, messages, and orchestration.
|
|
27
30
|
5. Declare the package provider and framework capabilities in `package.json`.
|
|
@@ -33,10 +36,10 @@ Read these package-owned source patterns:
|
|
|
33
36
|
The migration and resource are both authored contracts. A live schema is useful
|
|
34
37
|
for understanding an imported system, but it is never the sole source of truth.
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`
|
|
39
|
-
The `crud/
|
|
39
|
+
A CRUD contract shows and accepts only the fields its `fields` names, so give
|
|
40
|
+
every field the API shows or accepts its operations (a stored `createdAt` and
|
|
41
|
+
`updatedAt` are shown without an entry), and write a lookup field as `lookup: "<namespace>"`.
|
|
42
|
+
The `crud/crud-contract` and `database/table-resource` patterns list what each expands to.
|
|
40
43
|
|
|
41
44
|
## Ownership and access
|
|
42
45
|
|
|
@@ -51,7 +54,10 @@ The `crud/resource-contract` pattern lists what each expands to.
|
|
|
51
54
|
|
|
52
55
|
Normal CRUD tables use one non-null single-column primary key. Foreign keys are
|
|
53
56
|
single-column and target compatible keys. Composite unique indexes may enforce
|
|
54
|
-
business rules but do not become framework identities
|
|
57
|
+
business rules but do not become framework identities: a table keyed by several
|
|
58
|
+
columns, or by its owner's id, gets an integer `id` and keeps that key as a
|
|
59
|
+
unique index, read with a filter and written with an upsert
|
|
60
|
+
([Layer 1](../data-access.md#layer-1-the-table-resource)).
|
|
55
61
|
|
|
56
62
|
## Schema evolution
|
|
57
63
|
|
|
@@ -62,21 +68,26 @@ or rewrite a historical baseline.
|
|
|
62
68
|
|
|
63
69
|
## Customize without copying CRUD
|
|
64
70
|
|
|
65
|
-
`
|
|
71
|
+
`defineCrudFeature()` deliberately keeps application extension points:
|
|
66
72
|
|
|
73
|
+
- `owns` lists the feature's other tables, and the CRUD table's server-only
|
|
74
|
+
extras (`{ resource, rowPolicy, hooks }`).
|
|
67
75
|
- `decorateRepository` adds the few persistence operations unique to the
|
|
68
|
-
resource.
|
|
76
|
+
resource, on the table's handle.
|
|
69
77
|
- `decorateService` overrides standard methods or adds domain methods such as
|
|
70
78
|
`confirm`, `publish`, `cancel`, or `sendReminder`.
|
|
71
79
|
- `operationLifecycle` wraps a standard operation with `before`, `execute`,
|
|
72
80
|
`after`, and mutation-only `afterCommit` phases. Mutation phases before commit
|
|
73
|
-
receive the same
|
|
81
|
+
receive the same `options` (context and transaction) to pass on, and
|
|
82
|
+
`execute` receives `standard(nextInput)`. A deadlock reruns them, so they
|
|
83
|
+
touch only the database; an email or HTTP call goes in `afterCommit`, which
|
|
84
|
+
runs once.
|
|
74
85
|
- `actions` exposes non-CRUD service methods through normal validated,
|
|
75
86
|
permissioned, audited JSKIT actions and optional explicit HTTP routes.
|
|
76
87
|
|
|
77
88
|
A resource whose records carry a version number names it `lockVersionField`
|
|
78
|
-
and patches
|
|
79
|
-
|
|
89
|
+
and patches with the handle's `patch(id, changes, { ...options, lockVersion })`;
|
|
90
|
+
see the data access guide's Layer 4: locks.
|
|
80
91
|
|
|
81
92
|
Keep database reads and writes in repositories. Services and lifecycle hooks
|
|
82
93
|
orchestrate repositories; they do not issue raw database queries. Put external
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- Generated from `packages/agent-docs/heuristics/data-access.md`. -->
|
|
2
|
+
|
|
3
|
+
# Data access
|
|
4
|
+
|
|
5
|
+
Use when code reads or writes a database table: a repository, a batch, a lock,
|
|
6
|
+
a total or report, a transaction, a background job, or another feature's table.
|
|
7
|
+
|
|
8
|
+
Pick the layer ([Data access](../data-access.md)):
|
|
9
|
+
|
|
10
|
+
- One record, or one page of records: the owner's handle (`get`, `query`, `create`, `patch`, `delete`).
|
|
11
|
+
- Screens over HTTP: a CRUD contract on the same table resource.
|
|
12
|
+
- Many rows from one action: `createMany`, `patchMany`, `patchWhere`, `deleteWhere`; never a loop of single writes.
|
|
13
|
+
- Two requests could change the same rows: `lockMany` inside `withTransaction`, rows in id order.
|
|
14
|
+
- Work inside `withTransaction`: `txOptions` is only `{ context, trx }`, so a key the work needs, the app's own included, comes from the method's `options`, and a write on the method's own table gets `{ ...options, ...txOptions }` so the caller's `returning`, `missing` or `format` reaches it.
|
|
15
|
+
- A change only while a row still holds a value (open to booked, once): `patch` with `onlyIf`, or `patchWhere` with that value in its filters (a compare-and-set on every database, so of two concurrent writers one wins); a filter on a related table is refused on MySQL and MariaDB (`selection_cross_table`), so filter on the row's own columns, after locking the related rows with `lockMany`, or use `claim`.
|
|
16
|
+
- Counts, sums, buckets: `aggregate`; newest per group: a window include; anything else: one `scopedQuery()` method under `// Knex: <kind> - <reason>`.
|
|
17
|
+
- Every workspace or user at once: a job with `systemScope`, finding its units with an `aggregate` grouped by the ownership field and looping over them, never rows.
|
|
18
|
+
- A table another feature owns: its service or owner API, never its table. A report that joins it joins the `{ query, column }` its owner API answers from `scopedQuery({ alias })`, as a subquery: one statement, both scopes kept.
|
|
19
|
+
- Every path: a scope test and `expectFlatQueries`; a data test fails without `TEST_DB_NAME`.
|
|
@@ -65,8 +65,8 @@ Server-side pattern, only when implementing real backend filter semantics:
|
|
|
65
65
|
3. Pass the contract's `queryValidator` through the dedicated
|
|
66
66
|
`listFilterQueryValidator` option of the standard CRUD list validator group
|
|
67
67
|
at both the route and action boundaries.
|
|
68
|
-
4. Pass the contract's `jsonRestSearchSchema`
|
|
69
|
-
5.
|
|
68
|
+
4. Pass the contract's `jsonRestSearchSchema` as the `searchSchema` of the CRUD table's `owns` entry: `defineCrudFeature({ crud, owns: [{ resource, searchSchema: contract.jsonRestSearchSchema }] })`.
|
|
69
|
+
5. Override the service's `list` in `decorateService` so it calls `service.list(contract.toJsonRestQuery(query), options)`.
|
|
70
70
|
|
|
71
71
|
Exact file checklist:
|
|
72
72
|
- create `packages/<crud>/src/shared/<crud>ListFilters.js`
|
|
@@ -77,8 +77,8 @@ Exact file checklist:
|
|
|
77
77
|
- update `packages/<crud>/src/server/actions.js` so
|
|
78
78
|
`createStandardCrudListQueryValidators(...)` receives the same
|
|
79
79
|
`listFilterQueryValidator`
|
|
80
|
-
- update the
|
|
81
|
-
- update `
|
|
80
|
+
- update the feature's `owns` entry for the CRUD table so `searchSchema: listFilterContract.jsonRestSearchSchema` reaches the resource
|
|
81
|
+
- update the feature's `decorateService` so `list(query, options)` passes `listFilterContract.toJsonRestQuery(query)` to the standard `list`
|
|
82
82
|
- update the page-local `listFilters.js` first; only edit `index.vue` if a specialist lookup label/runtime integration is needed
|
|
83
83
|
- for lookup-backed filters, wire `useCrudListFilterLookups(...)` beside the existing filter runtime instead of replacing `CrudListFilterSurface`
|
|
84
84
|
|
|
@@ -168,8 +168,8 @@ Good shape:
|
|
|
168
168
|
- shared screen runtime passes `filterRuntime.queryParams` into the list request
|
|
169
169
|
- `packages/receivals/src/shared/receivalListFilters.js`
|
|
170
170
|
- `createCrudListFilterContract(RECEIVAL_LIST_FILTER_DEFINITIONS, { columns, invalidValues: "reject" })`
|
|
171
|
-
- `
|
|
172
|
-
- `
|
|
171
|
+
- `owns: [{ resource: receivalResource, searchSchema: receivalListFilterContract.jsonRestSearchSchema }]`
|
|
172
|
+
- `list: (query, options) => service.list(receivalListFilterContract.toJsonRestQuery(query), options)` in `decorateService`
|
|
173
173
|
- `const listFilters = useCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, { presets: [...] })`
|
|
174
174
|
- `const filterLookups = useCrudListFilterLookups(RECEIVAL_LIST_FILTER_DEFINITIONS, { values: listFilters.values, ... })`
|
|
175
175
|
- `queryParams: listFilters.queryParams`
|
|
@@ -6,43 +6,67 @@ Use when:
|
|
|
6
6
|
|
|
7
7
|
- mandatory record visibility needs `OR`, `EXISTS`, joins, memberships, or hierarchy traversal
|
|
8
8
|
- filtering after pagination produces empty or incomplete pages
|
|
9
|
-
- another package
|
|
9
|
+
- another package must narrow who sees a resource's rows but a reverse dependency would create a cycle
|
|
10
10
|
- the user mentions `RowPolicyPlugin`, recursive CTEs, descendants, or permission-filtered counts
|
|
11
11
|
- reads or writes of a resource with a policy get slower as records or a person's history grow
|
|
12
12
|
|
|
13
13
|
Read first:
|
|
14
14
|
|
|
15
|
-
- the owning
|
|
16
|
-
- the
|
|
17
|
-
- `packages/
|
|
15
|
+
- the owning feature's `owns` entry and the shared table resource
|
|
16
|
+
- the [data access](../data-access.md) guide's Layer 1
|
|
17
|
+
- `packages/resource-runtime/test/resourceRuntime.test.js` (policies, `ownTable`, contributions)
|
|
18
18
|
|
|
19
19
|
Default JSKIT pattern:
|
|
20
20
|
|
|
21
21
|
1. Keep public search and structured filters in the existing list-filter contract.
|
|
22
|
-
2. Keep direct user/workspace ownership and write stamping in
|
|
22
|
+
2. Keep direct user/workspace ownership and write stamping in the table resource's `ownership`.
|
|
23
23
|
3. Put complex mandatory read visibility in a server-only row policy.
|
|
24
|
-
4. Pass the policy
|
|
25
|
-
5. Derive identity from
|
|
24
|
+
4. Pass the policy in the owner's `owns` entry: `owns: [{ resource, rowPolicy }]`. It reads the owner's other tables through `ownTable(name)`, a read-only subquery, never by table name. Visibility rules live in the row policy, never in hand queries.
|
|
25
|
+
5. Derive identity from `caller` (`caller.kind`, then `caller.userId`), never from input. In a workspace request a role
|
|
26
|
+
rule reads `context.permissions`, which the server resolves from the membership before any handle runs, with
|
|
27
|
+
`hasPermission(context.permissions, "bookings.view-all")` from `@jskit-ai/kernel/shared/support`: `includes()`
|
|
28
|
+
misses wildcard grants, such as an owner role's `*` or a `bookings.*`.
|
|
26
29
|
6. Group every policy-owned `OR` branch.
|
|
27
30
|
7. Return `true` after adding the predicate, return `false` to deny all rows, or throw for invalid trusted context.
|
|
28
31
|
8. Keep action permissions for writes; visibility is not mutation authorization.
|
|
29
32
|
|
|
30
33
|
Package composition:
|
|
31
34
|
|
|
32
|
-
- the
|
|
33
|
-
-
|
|
34
|
-
- the resource owner
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- the owner seals the contributions and builds the policy during `boot()`
|
|
35
|
+
- the feature that owns the resource also owns the row policy
|
|
36
|
+
- the owner accepts contributions with `{ resource, rowPolicy, acceptsContributions: true }` in `owns`
|
|
37
|
+
- a package that narrows visibility further depends on the resource owner and calls `resources.contributeRowPolicy(name, policy)` on the handles its own `owns` gave it; a contribution is ANDed with the owner's policy inside its own group, so it can only narrow
|
|
38
|
+
- a contribution never grants visibility: wider visibility (shared rows, grants another package records) is a rule in the owner's own policy over its own tables, and a contribution narrows every read of the table, every feature's included, so a rule for one surface is an owner API method instead
|
|
39
|
+
- the resource owner does not import contributing packages
|
|
38
40
|
- do not use module-global mutable registries
|
|
39
41
|
|
|
42
|
+
Example, "your rooms or shared rooms":
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
owns: [{
|
|
46
|
+
resource: roomResource,
|
|
47
|
+
acceptsContributions: true,
|
|
48
|
+
rowPolicy({ query, caller, column, ownTable }) {
|
|
49
|
+
if (caller.kind !== "user") return caller.kind !== "anonymous";
|
|
50
|
+
query.where((rooms) => rooms
|
|
51
|
+
.where(column("shared"), true)
|
|
52
|
+
.orWhereIn(column("id"), ownTable("roomMembers").select("room_id").where("user_id", caller.userId)));
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A policy is told who the call runs for as `caller`: `user`, `anonymous`,
|
|
59
|
+
`system` or `job`. Background work is `system` or `job`, never a missing user,
|
|
60
|
+
and a job names its person only as `caller.actorId`. A policy hides a row from
|
|
61
|
+
every read, including other tables' links to it, so narrowing what one screen
|
|
62
|
+
lists is a filter on that screen's read.
|
|
63
|
+
|
|
40
64
|
Cost:
|
|
41
65
|
|
|
42
66
|
- a policy's cost follows the rows its query reads: add a join, `EXISTS` or subquery correlated with the row through `column()` and `value()`, on indexed columns; look at its SQL too, since a statement that runs to kilobytes is planned again on every read that carries it
|
|
43
|
-
- never load the subject's whole history (every source, notice or grant they were ever linked to) to build an `IN` list, and never call another owner's access check per item of it; another package's grant is a predicate
|
|
44
|
-
- the policy runs on every single-record read, including the read-back after `
|
|
45
|
-
- a write decides access once, inside its transaction; a batch write that would pay an expensive read-back per row follows the
|
|
67
|
+
- never load the subject's whole history (every source, notice or grant they were ever linked to) to build an `IN` list, and never call another owner's access check per item of it; another package's restriction is a predicate contribution, and another package's grant is a predicate in the owner's policy over facts the owner keeps
|
|
68
|
+
- the policy runs on every single-record read, including the read-back after a handle's `create()` and `patch()` (`returning: "record"`, their default) and the lookup before `patch` and `delete`, so a write pays it once per row it writes; the many-row writes check access once per statement
|
|
69
|
+
- a write decides access once, inside its transaction; a batch write that would pay an expensive read-back per row follows the data access guide's [Layer 3](../data-access.md#layer-3-many-rows-at-once)
|
|
46
70
|
|
|
47
71
|
Child-resource rules:
|
|
48
72
|
|