@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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
<!-- Generated from `packages/agent-docs/site/guide/app-setup/database-layer.md`. -->
|
|
2
|
+
|
|
3
|
+
# Database layer
|
|
4
|
+
|
|
5
|
+
JSKIT applications use Knex through one deliberately selected database driver.
|
|
6
|
+
Install either `@jskit-ai/database-runtime-mysql` or
|
|
7
|
+
`@jskit-ai/database-runtime-postgres`; do not install both unless the product
|
|
8
|
+
genuinely chooses a driver at runtime.
|
|
9
|
+
|
|
10
|
+
## Install the selected driver
|
|
11
|
+
|
|
12
|
+
For MySQL or MariaDB:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @jskit-ai/database-runtime @jskit-ai/database-runtime-mysql
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For PostgreSQL:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @jskit-ai/database-runtime @jskit-ai/database-runtime-postgres
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The application declares `@jskit-ai/database-runtime` itself: each driver takes
|
|
25
|
+
it as a peer, so the application holds its one copy, and brings its own Knex
|
|
26
|
+
driver through ordinary npm dependencies. There is no JSKIT install wizard.
|
|
27
|
+
|
|
28
|
+
## Connection environment
|
|
29
|
+
|
|
30
|
+
Keep credentials outside Git. Supply either `DATABASE_URL` or the individual
|
|
31
|
+
values:
|
|
32
|
+
|
|
33
|
+
```dotenv
|
|
34
|
+
DB_HOST=127.0.0.1
|
|
35
|
+
DB_PORT=3306
|
|
36
|
+
DB_NAME=exampleapp
|
|
37
|
+
DB_USER=exampleapp
|
|
38
|
+
DB_PASSWORD=secret
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use port `5432` for PostgreSQL. A single-driver app fixes its dialect in
|
|
42
|
+
`knexfile.js`, so it does not need `DB_CLIENT`. The running database provider
|
|
43
|
+
also derives its dialect from the installed driver and rejects a conflicting
|
|
44
|
+
`DB_CLIENT` value when one is supplied.
|
|
45
|
+
|
|
46
|
+
The application or hosting environment creates the database and provides its
|
|
47
|
+
credentials. JSKIT never guesses or commits them.
|
|
48
|
+
|
|
49
|
+
The running database provider registers a `database` health check that runs
|
|
50
|
+
`select 1` within two seconds. The foundations' `GET /api/health` answers `503`
|
|
51
|
+
while it fails; see [Health endpoint](/guide/app-setup/initial-scaffolding#health-endpoint).
|
|
52
|
+
|
|
53
|
+
## Migration configuration
|
|
54
|
+
|
|
55
|
+
Use the fixed-dialect pattern owned by the selected driver. The important
|
|
56
|
+
application file is small:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { createKnexMigrationConfigFromApp } from
|
|
60
|
+
"@jskit-ai/database-runtime/server/knexMigrationConfig";
|
|
61
|
+
|
|
62
|
+
export default await createKnexMigrationConfigFromApp({ client: "mysql2", environment: process.env });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `client: "pg"` for PostgreSQL. The environment is always passed: JSKIT reads
|
|
66
|
+
the connection values from what the application gives it, never from
|
|
67
|
+
`process.env` on its own. The complete examples are in:
|
|
68
|
+
|
|
69
|
+
- `database/mysql-application`
|
|
70
|
+
- `database/postgres-application`
|
|
71
|
+
|
|
72
|
+
Each example is complete for its own engine. Copy files only from the selected
|
|
73
|
+
driver's pattern and never merge the two; for example, the CI workflow must
|
|
74
|
+
contain only that engine's database service.
|
|
75
|
+
|
|
76
|
+
The config discovers migrations directly from the current installed package
|
|
77
|
+
graph. Application migrations live in the app's `migrations/` directory;
|
|
78
|
+
package-owned migrations live in directories declared by the installed
|
|
79
|
+
package's `package.json#jskit.migrations.directories`.
|
|
80
|
+
|
|
81
|
+
There is no migration sync command and no copied migration projection. Knex
|
|
82
|
+
runs the authoritative files where their owners ship them.
|
|
83
|
+
|
|
84
|
+
## What runs migrations
|
|
85
|
+
|
|
86
|
+
The application owns normal npm scripts:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"scripts": {
|
|
91
|
+
"db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
|
|
92
|
+
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
93
|
+
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`npm run db:migrate` runs the Knex CLI. Knex loads `knexfile.js`, discovers the
|
|
99
|
+
application and installed-package migration directories, connects using the
|
|
100
|
+
environment, and applies pending migrations.
|
|
101
|
+
|
|
102
|
+
Deployment or a managed development environment may invoke this app-owned
|
|
103
|
+
script as a release step. JSKIT itself does not maintain a background migration
|
|
104
|
+
service.
|
|
105
|
+
|
|
106
|
+
## Authoring schema changes
|
|
107
|
+
|
|
108
|
+
Write a new immutable migration in the package that owns the schema. Never edit
|
|
109
|
+
an already-applied migration and never change a live database without recording
|
|
110
|
+
the equivalent source-controlled migration.
|
|
111
|
+
|
|
112
|
+
Package migrations are normal `.cjs` Knex migrations and are declared in that
|
|
113
|
+
package's metadata:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"jskit": {
|
|
118
|
+
"migrations": {
|
|
119
|
+
"directories": ["migrations"]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A `constraints/` subdirectory of a declared directory, such as
|
|
126
|
+
`migrations/constraints/`, holds the package's constraints migrations; it runs
|
|
127
|
+
later, as described below.
|
|
128
|
+
|
|
129
|
+
A package whose table migrations use another package's tables names that
|
|
130
|
+
package in `after`, and its migrations, and its constraints, then run after
|
|
131
|
+
that package's:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"jskit": {
|
|
136
|
+
"migrations": {
|
|
137
|
+
"directories": ["migrations"],
|
|
138
|
+
"after": ["@jskit-ai/users-core"]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Each name in `after` must be another installed JSKIT package; a name that is
|
|
145
|
+
not installed, the package's own name, or packages that each name the other
|
|
146
|
+
stop discovery with an error. A named package without migrations orders
|
|
147
|
+
nothing.
|
|
148
|
+
|
|
149
|
+
Names must remain unique across the effective migration directories. Preserve
|
|
150
|
+
the basename when relocating a migration: Knex records that name in its ledger.
|
|
151
|
+
Do not rename it to force execution or edit an already-applied migration's body;
|
|
152
|
+
changed schema behavior requires a new migration.
|
|
153
|
+
|
|
154
|
+
### Directory order comes before filenames
|
|
155
|
+
|
|
156
|
+
`createKnexMigrationConfigFromApp()` sets `sortDirsSeparately: true` and runs
|
|
157
|
+
pending migrations in this order, using the default `migrationsDirectory`:
|
|
158
|
+
|
|
159
|
+
1. Discovered package migration directories, sorted by their absolute paths
|
|
160
|
+
using `localeCompare`, except that a package's directories follow those of
|
|
161
|
+
every package its `after` names, which follow theirs in turn, each in the
|
|
162
|
+
order listed. Without `after` the path order is unchanged.
|
|
163
|
+
2. Application `migrations/`.
|
|
164
|
+
3. The `constraints/` subdirectory of each discovered package migration
|
|
165
|
+
directory that has one, in the same order.
|
|
166
|
+
4. Application `migrations/constraints/`, when it exists.
|
|
167
|
+
|
|
168
|
+
Knex sorts filenames within each directory, not across the whole graph; see
|
|
169
|
+
its [`sortDirsSeparately` option](https://knexjs.org/guide/migrations.html#migration-api).
|
|
170
|
+
Package discovery does not read tables or foreign keys; only `after` changes
|
|
171
|
+
the package order. A newer timestamp in an earlier package still runs before
|
|
172
|
+
every file in a later package.
|
|
173
|
+
Inspect the effective `knexfile.js` configuration if the app customizes it.
|
|
174
|
+
|
|
175
|
+
Every package table exists before the application's own migrations run, so a
|
|
176
|
+
root `migrations/` file can reference a package table directly, for example a
|
|
177
|
+
`books.user_id` foreign key to `users.id`. A package's table migrations must
|
|
178
|
+
never depend on an application table.
|
|
179
|
+
|
|
180
|
+
Keep table and column creation with the owning package. Put foreign keys
|
|
181
|
+
between two packages, and other constraints that need the complete schema, in a
|
|
182
|
+
constraints phase: the referencing package's own `constraints/` subdirectory
|
|
183
|
+
when it ships the key, or application `migrations/constraints/`. A package
|
|
184
|
+
constraints migration runs after the application's `migrations/`, so it can also
|
|
185
|
+
reference a table that may belong to the application, such as `users`; for
|
|
186
|
+
example, `@jskit-ai/assistant-runtime` adds its `users` and `workspaces` keys
|
|
187
|
+
from `migrations/constraints/`. Similarly, a `pet_notes.product_id` column can be
|
|
188
|
+
added by the pet-notes package, but its foreign key to the products package's
|
|
189
|
+
`products.id` belongs in a constraints phase. Do not rely on the
|
|
190
|
+
package-directory path order to make a cross-package reference work; when a
|
|
191
|
+
package's table migrations need another package's table, declare `after`.
|
|
192
|
+
|
|
193
|
+
Until `@jskit-ai/database-runtime` 0.1.252, root `migrations/` ran before the
|
|
194
|
+
package directories, so a fresh database failed on any application migration
|
|
195
|
+
that referenced a package table. Applied migrations stay in the Knex ledger;
|
|
196
|
+
only pending ones follow the new order. An application migration that still
|
|
197
|
+
creates a package-owned table belongs in that package.
|
|
198
|
+
|
|
199
|
+
The assistant-runtime, console-core and rewarded-core table migrations used to
|
|
200
|
+
add their keys to `users`, `workspaces` and later tables inline. A fresh MySQL
|
|
201
|
+
or MariaDB database failed when the parent table did not exist yet (errno 150),
|
|
202
|
+
and the assistant tables silently skipped their `workspaces` keys. They now add
|
|
203
|
+
them from `migrations/constraints/`. A database migrated earlier keeps the keys
|
|
204
|
+
it already has, and gains any that are missing.
|
|
205
|
+
|
|
206
|
+
`@jskit-ai/database-runtime/server/foreignKeyMigrations` serves constraints
|
|
207
|
+
migrations that must run on fresh and already-migrated databases alike.
|
|
208
|
+
`addMissingForeignKey(knex, key)` adds a single-column key only when no key
|
|
209
|
+
already links that column to the parent column. It fails, naming the table,
|
|
210
|
+
column and count, when rows have no parent, and it deletes nothing; repair
|
|
211
|
+
those rows and migrate again. `dropForeignKeyIfPresent(knex, key)` serves
|
|
212
|
+
`down()`.
|
|
213
|
+
|
|
214
|
+
Before adding the key, check that both tables and columns exist, the referenced
|
|
215
|
+
key is indexed appropriately, column types match (including integer signedness),
|
|
216
|
+
existing values have valid parents, and `ON DELETE SET NULL` uses a nullable
|
|
217
|
+
column. An incorrectly formed foreign key is not always an ordering failure.
|
|
218
|
+
Do not suppress the failure, skip a missing parent, or disable foreign-key
|
|
219
|
+
checks to make preparation appear successful.
|
|
220
|
+
|
|
221
|
+
### MySQL and MariaDB failures can leave partial schema
|
|
222
|
+
|
|
223
|
+
MySQL and MariaDB DDL can commit implicitly. If adding a column succeeds and a
|
|
224
|
+
later statement adding its foreign key fails, the column can remain even though
|
|
225
|
+
Knex has not recorded that migration as completed. A Knex transaction does not
|
|
226
|
+
make a multi-statement DDL migration atomic. See the
|
|
227
|
+
[MySQL implicit-commit rules](https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html)
|
|
228
|
+
and [MariaDB implicit-commit rules](https://mariadb.com/docs/server/reference/sql-statements/transactions/sql-statements-that-cause-an-implicit-commit).
|
|
229
|
+
|
|
230
|
+
Design new multi-step DDL migrations for their concrete partial states. Inspect
|
|
231
|
+
the column and foreign key independently; `hasColumn()` alone cannot prove the
|
|
232
|
+
constraint exists. Verify the definition of an existing object before skipping
|
|
233
|
+
its creation, complete only missing work, and fail clearly on incompatible
|
|
234
|
+
schema. Do not catch arbitrary SQL errors as success or mark incomplete work
|
|
235
|
+
as applied. Keep recovery local to the affected migration or an explicit repair;
|
|
236
|
+
do not add schema repair to request handlers or normal server startup.
|
|
237
|
+
|
|
238
|
+
For an existing failure, inspect the ledger and actual schema read-only first.
|
|
239
|
+
Preserve applied migration bodies and use a new corrective migration for
|
|
240
|
+
databases that already recorded them. If an earlier pending migration prevents
|
|
241
|
+
that correction from running, describe the required backed-up, explicit repair
|
|
242
|
+
before retrying; a later file alone cannot repair a blocked earlier step. Any
|
|
243
|
+
relocation must retain its basename and be checked against both fresh and
|
|
244
|
+
existing ledgers. Never drop valuable data or delete ledger entries to force
|
|
245
|
+
a replay.
|
|
246
|
+
|
|
247
|
+
## Seed data is not a migration
|
|
248
|
+
|
|
249
|
+
Migrations establish schema and invariant framework data. Product fixtures,
|
|
250
|
+
sample accounts, catalog content, and other environment-specific starting data
|
|
251
|
+
belong in an explicit, idempotent application seed operation that runs after
|
|
252
|
+
migrations. Do not hide product seeding in schema migrations.
|
|
253
|
+
|
|
254
|
+
Managed editors must be able to create one isolated database per development
|
|
255
|
+
session, apply the full migration graph, then invoke that explicit seed
|
|
256
|
+
operation. JSKIT supplies portable migration and seed seams; the editor owns
|
|
257
|
+
database allocation, credentials, lifetime, and environment injection.
|
|
258
|
+
|
|
259
|
+
## Resource services and custom operations
|
|
260
|
+
|
|
261
|
+
Conventional persisted resources use `defineTableResource()`,
|
|
262
|
+
`defineCrudContract()` and `defineCrudFeature()` so the framework owns
|
|
263
|
+
repeated table ownership, service, action, permission, JSON API, and route
|
|
264
|
+
mechanics. This does not make product CRUD behavior fixed.
|
|
265
|
+
|
|
266
|
+
- `decorateRepository` adds resource-specific queries, locks, or writes.
|
|
267
|
+
- `decorateService` overrides a standard method or adds domain methods such as
|
|
268
|
+
`confirm`, `publish`, `cancel`, or `sendReminder`.
|
|
269
|
+
- `operationLifecycle` surrounds a standard operation with `before`, `execute`,
|
|
270
|
+
`after`, and mutation-only `afterCommit` phases. Create, update, and delete
|
|
271
|
+
phases before commit share one repository transaction, and `execute` receives
|
|
272
|
+
`standard(nextInput)` for retaining the normal framework write. A deadlock
|
|
273
|
+
runs that transaction again, so `before`, `execute` and `after` can run twice
|
|
274
|
+
and touch only the database; an outside effect (an email, an HTTP call) goes
|
|
275
|
+
in `afterCommit`, which runs once after commit, or in an outbox row.
|
|
276
|
+
- Named `actions` expose non-CRUD service methods through normal input,
|
|
277
|
+
permission, event, and optional HTTP route contracts.
|
|
278
|
+
|
|
279
|
+
The feature that owns a table reads and writes it through the handles its
|
|
280
|
+
`owns` entry gives its repository; services, hooks and other features call
|
|
281
|
+
that repository or the owner's API, never the table. External delivery belongs after commit; when it must be durable,
|
|
282
|
+
write an outbox record inside the transaction and deliver it separately.
|
|
283
|
+
A separate Feature is warranted when an operation belongs to another domain,
|
|
284
|
+
not merely because a useful resource has behavior beyond list and save. Write
|
|
285
|
+
it as a [typed feature](typed-features.md).
|
|
286
|
+
|
|
287
|
+
## Writes, versions, locks and deadlocks
|
|
288
|
+
|
|
289
|
+
Writes that skip their read-back (`returning`), optimistic locking
|
|
290
|
+
(`lockVersionField`), row locks and deadlock retries belong to the table
|
|
291
|
+
resource's handle: see [Data access](data-access.md), Layers 1
|
|
292
|
+
and 4 and [Transactions](data-access.md#transactions).
|
|
293
|
+
|
|
294
|
+
## Verification
|
|
295
|
+
|
|
296
|
+
- For every schema change, run the normal preparation entrypoint against a
|
|
297
|
+
fresh disposable database on the selected engine with the complete package
|
|
298
|
+
graph. An existing development database can hide ordering failures.
|
|
299
|
+
- In a separate disposable fixture at the previous schema and ledger, apply
|
|
300
|
+
the upgrade and prove representative existing rows survive.
|
|
301
|
+
- For multi-step MySQL or MariaDB DDL, reproduce partial completion (for
|
|
302
|
+
example, column present, foreign key absent, migration still pending), retry
|
|
303
|
+
preparation, and verify the final column and constraint definitions.
|
|
304
|
+
- Run preparation again and `npm run db:migrate:status`; require no pending
|
|
305
|
+
migrations. Prove invalid references are rejected and the declared delete
|
|
306
|
+
behavior works. A stubbed Knex test or successful process exit is insufficient.
|
|
307
|
+
- Exercise a real transaction and one invalid-connection case.
|
|
308
|
+
- When a seed operation exists, run it twice and require the second run to be
|
|
309
|
+
safe.
|
|
310
|
+
- Use the application's selected engine; exercise each supported engine when
|
|
311
|
+
changing a shared pattern. Report unavailable database verification explicitly.
|
|
312
|
+
|
|
313
|
+
Do not add migration receipts, sync ledgers, generator provenance, or dialect
|
|
314
|
+
questionnaires. The installed graph, migration source, environment, and
|
|
315
|
+
database migration table are sufficient.
|
|
316
|
+
|
|
317
|
+
## Integration test databases
|
|
318
|
+
|
|
319
|
+
Disposable integration and migration tests use `TEST_DB_NAME`, never the
|
|
320
|
+
application's `DB_NAME`. A test run prepares that schema once and restores it
|
|
321
|
+
from a stored snapshot. `jskit test --databases` (the database patterns' `test`
|
|
322
|
+
script) does this with `@jskit-ai/database-runtime/server/testDatabases`, from
|
|
323
|
+
the application's `knexfile.js`:
|
|
324
|
+
|
|
325
|
+
- Each test file running at once gets a database of its own for as long as it
|
|
326
|
+
runs, as both `DB_NAME` and `TEST_DB_NAME`: `TEST_DB_NAME` itself, then
|
|
327
|
+
copies named `<TEST_DB_NAME>_1`, `_2`, ... up to the machine's test budget,
|
|
328
|
+
as many as the database user may create. With no usable `TEST_DB_NAME`
|
|
329
|
+
(unset, the connected database itself, or one the database user may not
|
|
330
|
+
prepare), the files run one at a time on the connected database. A copy
|
|
331
|
+
named like the application's database or `PROD_DB_NAME`, or a server that
|
|
332
|
+
cannot be reached, fails the run instead.
|
|
333
|
+
- The snapshot holds the schema as the engine reports it and the rows the
|
|
334
|
+
migrations wrote, including the migration ledger. On MySQL and MariaDB it is
|
|
335
|
+
each table's `SHOW CREATE TABLE` and rows, in
|
|
336
|
+
`node_modules/.cache/jskit-test-databases/`; on PostgreSQL it is the database
|
|
337
|
+
`<TEST_DB_NAME>_snapshot`, which `CREATE DATABASE <name> TEMPLATE <snapshot>`
|
|
338
|
+
copies while nothing is connected to it (the user needs `CREATEDB`), file by
|
|
339
|
+
file (`STRATEGY FILE_COPY`) from PostgreSQL 15. Its key
|
|
340
|
+
is a digest of the dialect, of the name and contents of every migration file
|
|
341
|
+
that the application's `migrations/` and its installed packages supply, and
|
|
342
|
+
of `package-lock.json`, since an upgraded package can change a helper its
|
|
343
|
+
migrations import. MySQL test databases hold tables only: a schema with
|
|
344
|
+
views, triggers, routines or events is refused.
|
|
345
|
+
- Each test file gets the environment `jskit test` was given, plus its
|
|
346
|
+
database's connection (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`); the
|
|
347
|
+
rest of the `.env` that `knexfile.js` loads is read for the databases only.
|
|
348
|
+
The run's settings, connection password included, are in a file only the
|
|
349
|
+
user can read; the runner's environment names only its directory.
|
|
350
|
+
- When that digest changes, the harness replays the migrations into the
|
|
351
|
+
designated database once, outside the timed tests, and stores a new
|
|
352
|
+
snapshot. Otherwise it restores the stored one.
|
|
353
|
+
- A schema the migrations alone do not build, such as migrations that start
|
|
354
|
+
from a baseline schema file, or reference rows every test reads, comes from
|
|
355
|
+
the application's own build: `knexfile.js` also exports
|
|
356
|
+
`testDatabaseSchema = { files, build }`, and a browser `prepare` module passes
|
|
357
|
+
the same `schema` to `prepareTestDatabases()`. `build(knex)` runs in the empty
|
|
358
|
+
designated database instead of `knex.migrate.latest()` (the baseline, then
|
|
359
|
+
`knex.migrate.latest()`), and `files` lists every file it reads, its own
|
|
360
|
+
module included, relative to the application root: their contents join the
|
|
361
|
+
digest, so changing one takes a new snapshot.
|
|
362
|
+
- A test that runs migrations up or down, or otherwise changes the tables,
|
|
363
|
+
leaves its database dirty: its tables or migration ledger differ from the
|
|
364
|
+
snapshot's. The harness restores the snapshot before another test file uses
|
|
365
|
+
that database, instead of running `migrate.latest()` over the whole history.
|
|
366
|
+
- A browser run's seed is not part of the snapshot: it runs in each restored
|
|
367
|
+
database, in one transaction ([Browser test preparation](#browser-test-preparation)).
|
|
368
|
+
- Only the fresh-schema check in [Verification](#verification) replays the
|
|
369
|
+
whole history, as its own step.
|
|
370
|
+
- The harness reports each phase (digest, restore or rebuild, seed) with its
|
|
371
|
+
duration, and those durations are read, so slow preparation is visible
|
|
372
|
+
instead of hiding inside the next test that waits for it. A slow phase is
|
|
373
|
+
fixed, never absorbed by a longer time limit.
|
|
374
|
+
|
|
375
|
+
Endpoints and batch operations get their own database tests at realistic row
|
|
376
|
+
counts that compare their query counts, counted with `countQueries()` from
|
|
377
|
+
`@jskit-ai/database-runtime/server/testDatabases`, or `countRuntimeQueries()`
|
|
378
|
+
for a route of the server the test boots; see the typed features guide's
|
|
379
|
+
[data access tests](data-access.md#tests).
|
|
380
|
+
|
|
381
|
+
## Browser test preparation
|
|
382
|
+
|
|
383
|
+
A browser run gives every Playwright worker its own server and its own copy of
|
|
384
|
+
`BROWSER_TEST_DB_NAME`. The database patterns' `example/tests/e2e/databases.js`
|
|
385
|
+
is the `prepare` module of `browserTestConfig({ prepare: "tests/e2e/databases.js" })`
|
|
386
|
+
from `@jskit-ai/jskit-catalog/browser-tests`: it calls `prepareTestDatabases()`
|
|
387
|
+
with `variable: "BROWSER_TEST_DB_NAME"`, the worker count and the app's fixture
|
|
388
|
+
seed, and returns each worker's `DB_NAME`. The worker's own process gets the
|
|
389
|
+
same environment as its server, so a test that opens the database itself (with
|
|
390
|
+
the app's `knexfile.js`) reaches that worker's copy. The copies (`<name>`, `<name>_1`,
|
|
391
|
+
...) are restored from the same stored snapshot as the Node test databases
|
|
392
|
+
([Integration test databases](#integration-test-databases)), then seeded in one
|
|
393
|
+
transaction each, so stale rows never survive a run and the migrations are
|
|
394
|
+
replayed only when one changes. It refuses the application's database,
|
|
395
|
+
`PROD_DB_NAME`, `TEST_DB_NAME` and their copies.
|
|
396
|
+
|
|
397
|
+
Give disposable integration and migration tests their own `TEST_DB_NAME`;
|
|
398
|
+
the two names, and their copies, must differ, and the application account
|
|
399
|
+
needs grants for each (a `<app>_%` grant covers the copies). Each preparation
|
|
400
|
+
reports how many databases it made, whether it replayed the migrations for a
|
|
401
|
+
new snapshot, and its restore and seed durations, so a cold rebuild is visible
|
|
402
|
+
to the caller.
|
|
403
|
+
|
|
404
|
+
Run related checks in one suite invocation and prepare only their required
|
|
405
|
+
fixtures. A local browser run serves the production build, never the
|
|
406
|
+
development launcher; fresh-schema migration proofs are separate. A managed
|
|
407
|
+
suite (`PLAYWRIGHT_BASE_URL`) uses the host's server URL and identity, as the
|
|
408
|
+
host serves it, starts no second server, and restores normal Preview after the
|
|
409
|
+
batch. Database isolation alone does not
|
|
410
|
+
disable email, payments or other external effects: configure those test controls
|
|
411
|
+
and verify the actual server identity before destructive requests.
|
|
@@ -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,113 @@ 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)).
|
|
236
269
|
|
|
237
270
|
### Workspace invitation transaction compatibility (workspaces-core 0.1.198)
|
|
238
271
|
|