@jskit-ai/agent-docs 0.1.283 → 0.1.284
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 +64 -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 +105 -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 +8 -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 +1031 -0
- package/guide/agent/framework/sanity-checks.md +26 -27
- package/guide/agent/framework/typed-features.md +114 -247
- 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 +2 -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 +65 -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 +38 -173
- package/reference/autogen/packages/database-runtime.md +8 -32
- package/reference/autogen/packages/http-runtime.md +3 -3
- package/reference/autogen/packages/http-web.md +12 -0
- package/reference/autogen/packages/kernel.md +25 -2
- package/reference/autogen/packages/oidc-issuer-core.md +4 -0
- package/reference/autogen/packages/payments-core.md +29 -6
- package/reference/autogen/packages/realtime.md +15 -6
- package/reference/autogen/packages/resource-core.md +30 -5
- package/reference/autogen/packages/resource-crud-core.md +30 -34
- package/reference/autogen/packages/resource-runtime.md +198 -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 +26 -14
- package/reference/autogen/packages/workspaces-core.md +17 -10
- package/reference/autogen/tooling/config-eslint.md +69 -0
- package/reference/autogen/tooling/jskit-catalog.md +36 -1
- package/reference/autogen/tooling/jskit-release.md +3 -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 +1026 -0
- package/skills/jskit/references/database-layer.md +411 -0
- package/skills/jskit/references/existing-application-migration.md +105 -72
- package/skills/jskit/references/heuristics/INDEX.md +6 -4
- package/skills/jskit/references/heuristics/crud-authoring.md +22 -14
- 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 +117 -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 +59 -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 +114 -247
- 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
|
@@ -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
|
|
|
@@ -62,21 +65,26 @@ or rewrite a historical baseline.
|
|
|
62
65
|
|
|
63
66
|
## Customize without copying CRUD
|
|
64
67
|
|
|
65
|
-
`
|
|
68
|
+
`defineCrudFeature()` deliberately keeps application extension points:
|
|
66
69
|
|
|
70
|
+
- `owns` lists the feature's other tables, and the CRUD table's server-only
|
|
71
|
+
extras (`{ resource, rowPolicy, hooks }`).
|
|
67
72
|
- `decorateRepository` adds the few persistence operations unique to the
|
|
68
|
-
resource.
|
|
73
|
+
resource, on the table's handle.
|
|
69
74
|
- `decorateService` overrides standard methods or adds domain methods such as
|
|
70
75
|
`confirm`, `publish`, `cancel`, or `sendReminder`.
|
|
71
76
|
- `operationLifecycle` wraps a standard operation with `before`, `execute`,
|
|
72
77
|
`after`, and mutation-only `afterCommit` phases. Mutation phases before commit
|
|
73
|
-
receive the same
|
|
78
|
+
receive the same `options` (context and transaction) to pass on, and
|
|
79
|
+
`execute` receives `standard(nextInput)`. A deadlock reruns them, so they
|
|
80
|
+
touch only the database; an email or HTTP call goes in `afterCommit`, which
|
|
81
|
+
runs once.
|
|
74
82
|
- `actions` exposes non-CRUD service methods through normal validated,
|
|
75
83
|
permissioned, audited JSKIT actions and optional explicit HTTP routes.
|
|
76
84
|
|
|
77
85
|
A resource whose records carry a version number names it `lockVersionField`
|
|
78
|
-
and patches
|
|
79
|
-
|
|
86
|
+
and patches with the handle's `patch(id, changes, { ...options, lockVersion })`;
|
|
87
|
+
see the data access guide's Layer 4: locks.
|
|
80
88
|
|
|
81
89
|
Keep database reads and writes in repositories. Services and lifecycle hooks
|
|
82
90
|
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
|
|
|
@@ -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)
|