@jskit-ai/agent-docs 0.1.131 → 0.1.132

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.
Files changed (170) hide show
  1. package/guide/agent/app-extras/assistant.md +29 -605
  2. package/guide/agent/app-extras/mobile-capacitor.md +29 -362
  3. package/guide/agent/app-extras/realtime.md +29 -277
  4. package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
  5. package/guide/agent/app-setup/authentication.md +43 -1073
  6. package/guide/agent/app-setup/console.md +26 -298
  7. package/guide/agent/app-setup/database-layer.md +110 -790
  8. package/guide/agent/app-setup/initial-scaffolding.md +50 -784
  9. package/guide/agent/app-setup/multi-homing.md +39 -712
  10. package/guide/agent/app-setup/quickstart.md +43 -179
  11. package/guide/agent/app-setup/users.md +34 -353
  12. package/guide/agent/index.md +16 -23
  13. package/package.json +2 -2
  14. package/patterns/INDEX.md +7 -7
  15. package/patterns/child-cruds.md +3 -3
  16. package/patterns/client-requests.md +6 -6
  17. package/patterns/crud-authoring.md +94 -0
  18. package/patterns/crud-links.md +1 -1
  19. package/patterns/feature-package/PATTERN.md +108 -0
  20. package/patterns/feature-package/example/booking-engine/package.json +48 -0
  21. package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
  22. package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
  23. package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
  24. package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
  25. package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
  26. package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
  27. package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
  28. package/patterns/filters.md +8 -8
  29. package/patterns/live-actions.md +5 -18
  30. package/patterns/minimal-foundation/PATTERN.md +98 -0
  31. package/patterns/minimal-foundation/example/.nvmrc +1 -0
  32. package/patterns/minimal-foundation/example/AGENTS.md +17 -0
  33. package/patterns/minimal-foundation/example/Procfile +2 -0
  34. package/patterns/minimal-foundation/example/app.json +14 -0
  35. package/patterns/minimal-foundation/example/bin/develop.js +71 -0
  36. package/patterns/minimal-foundation/example/bin/server.js +8 -0
  37. package/patterns/minimal-foundation/example/config/public.js +40 -0
  38. package/patterns/minimal-foundation/example/config/server.js +1 -0
  39. package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
  40. package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
  41. package/patterns/minimal-foundation/example/favicon.svg +7 -0
  42. package/patterns/minimal-foundation/example/gitignore +9 -0
  43. package/patterns/minimal-foundation/example/index.html +13 -0
  44. package/patterns/minimal-foundation/example/jsconfig.json +8 -0
  45. package/patterns/minimal-foundation/example/package.json +57 -0
  46. package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
  47. package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
  48. package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  49. package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
  50. package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
  51. package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
  52. package/patterns/minimal-foundation/example/server.js +195 -0
  53. package/patterns/minimal-foundation/example/src/App.vue +13 -0
  54. package/patterns/minimal-foundation/example/src/main.js +85 -0
  55. package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
  56. package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
  57. package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
  58. package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
  59. package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  60. package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
  61. package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
  62. package/patterns/page-scaffolding.md +20 -17
  63. package/patterns/placements.md +17 -15
  64. package/patterns/row-policies.md +4 -5
  65. package/patterns/server-search.md +3 -3
  66. package/patterns/shell-foundation/PATTERN.md +104 -0
  67. package/patterns/shell-foundation/example/.nvmrc +1 -0
  68. package/patterns/shell-foundation/example/AGENTS.md +17 -0
  69. package/patterns/shell-foundation/example/Procfile +2 -0
  70. package/patterns/shell-foundation/example/app.json +14 -0
  71. package/patterns/shell-foundation/example/bin/develop.js +71 -0
  72. package/patterns/shell-foundation/example/bin/server.js +8 -0
  73. package/patterns/shell-foundation/example/config/public.js +40 -0
  74. package/patterns/shell-foundation/example/config/server.js +1 -0
  75. package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
  76. package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
  77. package/patterns/shell-foundation/example/favicon.svg +7 -0
  78. package/patterns/shell-foundation/example/gitignore +9 -0
  79. package/patterns/shell-foundation/example/index.html +13 -0
  80. package/patterns/shell-foundation/example/jsconfig.json +8 -0
  81. package/patterns/shell-foundation/example/package.json +59 -0
  82. package/patterns/shell-foundation/example/packages/main/package.json +56 -0
  83. package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
  84. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
  85. package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
  86. package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  87. package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
  88. package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
  89. package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
  90. package/patterns/shell-foundation/example/server.js +195 -0
  91. package/patterns/shell-foundation/example/src/App.vue +11 -0
  92. package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
  93. package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
  94. package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  95. package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
  96. package/patterns/shell-foundation/example/src/error.js +19 -0
  97. package/patterns/shell-foundation/example/src/main.js +85 -0
  98. package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
  99. package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
  100. package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
  101. package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
  102. package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
  103. package/patterns/shell-foundation/example/src/placement.js +56 -0
  104. package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
  105. package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
  106. package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
  107. package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
  108. package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  109. package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
  110. package/patterns/shell-foundation/example/vite.config.mjs +81 -0
  111. package/patterns/ui-contract.md +56 -0
  112. package/patterns/ui-testing.md +10 -12
  113. package/reference/autogen/KERNEL_MAP.md +29 -107
  114. package/reference/autogen/PATTERN_INDEX.md +230 -0
  115. package/reference/autogen/README.md +4 -8
  116. package/reference/autogen/packages/agent-docs.md +259 -0
  117. package/reference/autogen/packages/assistant-core.md +3 -3
  118. package/reference/autogen/packages/assistant-runtime.md +32 -17
  119. package/reference/autogen/packages/auth-core.md +31 -33
  120. package/reference/autogen/packages/auth-provider-local-core.md +4 -12
  121. package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
  122. package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
  123. package/reference/autogen/packages/auth-web.md +42 -22
  124. package/reference/autogen/packages/console-core.md +8 -25
  125. package/reference/autogen/packages/console-web.md +5 -5
  126. package/reference/autogen/packages/crud-core.md +61 -17
  127. package/reference/autogen/packages/database-runtime-mysql.md +12 -2
  128. package/reference/autogen/packages/database-runtime-postgres.md +12 -2
  129. package/reference/autogen/packages/database-runtime.md +26 -25
  130. package/reference/autogen/packages/google-rewarded-core.md +19 -104
  131. package/reference/autogen/packages/http-runtime.md +4 -8
  132. package/reference/autogen/packages/http-web.md +32 -0
  133. package/reference/autogen/packages/json-rest-api-core.md +4 -6
  134. package/reference/autogen/packages/kernel.md +109 -390
  135. package/reference/autogen/packages/mobile-capacitor.md +2 -13
  136. package/reference/autogen/packages/realtime.md +29 -26
  137. package/reference/autogen/packages/resource-crud-core.md +6 -0
  138. package/reference/autogen/packages/shell-web.md +69 -54
  139. package/reference/autogen/packages/storage-runtime.md +3 -3
  140. package/reference/autogen/packages/uploads-image-web.md +0 -1
  141. package/reference/autogen/packages/uploads-runtime.md +3 -3
  142. package/reference/autogen/packages/users-core.md +45 -90
  143. package/reference/autogen/packages/users-web.md +5 -7
  144. package/reference/autogen/packages/workspaces-core.md +53 -74
  145. package/reference/autogen/packages/workspaces-web.md +15 -16
  146. package/reference/autogen/tooling/jskit-catalog.md +34 -0
  147. package/reference/autogen/tooling/testUtils.md +4 -4
  148. package/skills/jskit/SKILL.md +34 -29
  149. package/skills/jskit/references/app-operations.md +68 -53
  150. package/skills/jskit/references/crud-operations.md +58 -106
  151. package/skills/jskit/references/material-3.md +1 -1
  152. package/skills/jskit/references/ui-operations.md +41 -44
  153. package/templates/app/AGENTS.md +7 -3
  154. package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
  155. package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
  156. package/guide/agent/generators/advanced-cruds.md +0 -1935
  157. package/guide/agent/generators/crud-generators.md +0 -948
  158. package/guide/agent/generators/intro.md +0 -65
  159. package/guide/agent/generators/row-policies.md +0 -537
  160. package/guide/agent/generators/ui-generators.md +0 -690
  161. package/patterns/crud-scaffolding.md +0 -198
  162. package/patterns/generated-ui-contract-tracking.md +0 -66
  163. package/reference/autogen/packages/assistant.md +0 -68
  164. package/reference/autogen/packages/crud-server-generator.md +0 -215
  165. package/reference/autogen/packages/crud-ui-generator.md +0 -192
  166. package/reference/autogen/packages/feature-server-generator.md +0 -65
  167. package/reference/autogen/packages/ui-generator.md +0 -127
  168. package/reference/autogen/tooling/create-app.md +0 -317
  169. package/reference/autogen/tooling/jskit-cli.md +0 -933
  170. package/reference/autogen/tooling/test-support.md +0 -27
@@ -2,632 +2,34 @@
2
2
 
3
3
  # Database layer
4
4
 
5
- At the end of the previous chapter, the app could already authenticate real users through the local auth provider, without requiring Supabase or a database. In this chapter, we install the MySQL database runtime, add the migration tooling, and explain what that changes immediately and what it still does **not** change yet.
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.
6
9
 
7
- This chapter is more infrastructural than the previous ones. That is intentional. There is no dramatic new screen in the browser. The important change is that the app gains a real database layer that later packages can depend on.
10
+ ## Install the selected driver
8
11
 
9
- ## Installing the database runtime
10
-
11
- From inside `exampleapp`, run:
12
-
13
- ```bash
14
- DB_HOST=127.0.0.1
15
- DB_PORT=3306
16
- DB_NAME=exampleapp
17
- DB_USER=exampleapp
18
- DB_PASSWORD=secret
19
-
20
- npx jskit add package database-runtime-mysql \
21
- --db-host "$DB_HOST" \
22
- --db-port "$DB_PORT" \
23
- --db-name "$DB_NAME" \
24
- --db-user "$DB_USER" \
25
- --db-password "$DB_PASSWORD"
26
- ```
27
-
28
- The first command adds the MySQL driver package and its generic JSKIT database runtime dependency, using the explicit connection values from those `DB_*` variables:
29
-
30
- - database host
31
- - database port
32
- - database name
33
- - database user
34
- - database password
35
-
36
- The second command downloads the newly referenced runtime packages and the underlying Node dependencies, especially `knex` and `mysql2`.
37
-
38
- If you open the app in the browser after this install, it still looks the same. That is expected.
39
-
40
- - `/home` still renders the same shell
41
- - `/auth/login` still renders the same login screen
42
-
43
- This chapter changes the server-side infrastructure of the app, not the visible shell.
44
-
45
- ## What changes immediately
46
-
47
- Installing `database-runtime-mysql` gives the app three important things right away.
48
-
49
- ### A real database runtime
50
-
51
- The server can build a real Knex client from environment variables. That gives later packages a standard way to ask for a database connection instead of each package inventing its own wiring.
52
-
53
- ### A migration workflow
54
-
55
- The app gets three database scripts in `package.json`:
56
-
57
- ```json
58
- {
59
- "scripts": {
60
- "db:migrations:sync": "jskit migrations sync",
61
- "db:migrate": "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:latest",
62
- "db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
63
- "db:migrate:status": "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:list"
64
- }
65
- }
66
- ```
67
-
68
- That is the first time the scaffold can talk about schema migrations in a standard way.
69
-
70
- If you run the status command immediately after this chapter:
71
-
72
- ```bash
73
- npm run db:migrate:status
74
- ```
75
-
76
- you should still see that there are no completed migrations and no pending migration files yet. The runtime and the Knex wiring exist at this point, but no package has added real schema files until the next chapter.
77
-
78
- ### A place for future schema files
79
-
80
- The app also gets a top-level `migrations/` directory. Right now it only contains `.gitkeep`, which can look underwhelming at first, but that empty directory is actually useful. It means the migration toolchain is ready even before any package installs real schema files.
81
-
82
- That is the key idea of this chapter:
83
-
84
- - the database runtime provides the **infrastructure**
85
- - later packages provide the **actual schema**
86
-
87
- ## Two migration steps, two different tools
88
-
89
- This chapter is the right place to make one distinction very explicit.
90
-
91
- The app has two different migration-related layers:
92
-
93
- - JSKIT-managed migration files on disk
94
- - Knex actually applying those files to the database
95
-
96
- Those are **not** the same step.
97
-
98
- There is also an important ownership distinction:
99
-
100
- - a CRUD generator owns the installed baseline migration for the table it
101
- scaffolds
102
- - the table's app-local package owns later additive schema evolution
103
-
104
- Never modify or replace a generator-owned baseline migration. Later schema
105
- evolution must use a new immutable, package-owned additive migration in the
106
- table's app-local package, declared through `install-migration`.
107
-
108
- The npm scripts run `npm run db:migrations:sync` first, then run Knex. That means newly installed package migrations are present before Knex checks what is pending.
109
-
110
- ### `jskit migrations sync` writes package migration files
111
-
112
- If you run the sync script directly:
113
-
114
- ```bash
115
- npm run db:migrations:sync
116
- ```
117
-
118
- JSKIT reads `install-migration` entries from `package.json.jskit` across the installed npm graph and writes any missing immutable migration files into `migrations/`.
119
-
120
- That command is about the app scaffold on disk.
121
-
122
- It does **not** run Knex against MySQL or Postgres.
123
-
124
- At this point in the guide, that command will usually touch nothing yet, because `database-runtime-mysql` gives you the migration **tooling** and `migrations/.gitkeep`, but it does not add real schema files of its own.
125
-
126
- ### `npm run db:migrate` runs Knex against the database
127
-
128
- When you run:
129
-
130
- ```bash
131
- npm run db:migrate
132
- ```
133
-
134
- the Knex CLI reads `knexfile.js`, connects to the real database, and applies any pending migration files it finds.
135
-
136
- That command is about the actual database.
137
-
138
- So the clean mental model is:
139
-
140
- 1. JSKIT writes or refreshes managed migration files into `migrations/`
141
- 2. Knex applies those files to MySQL or Postgres
142
-
143
- ### When you need each step
144
-
145
- In normal `jskit add package ...` flows, JSKIT synchronizes package migration files after installation.
146
-
147
- That means the most common flow is still:
148
-
149
- ```bash
150
- npx jskit add package users-web
151
- npm run db:migrate
152
- ```
153
-
154
- You can also synchronize explicitly:
155
-
156
- ```bash
157
- npx jskit migrations sync
158
- npm run db:migrate
159
- ```
160
-
161
- So:
162
-
163
- - use `jskit migrations sync` when you need JSKIT to write package migration files
164
- - use `npm run db:migrate` when you need Knex to apply pending migration files to the real database
165
- - sometimes you need only `npm run db:migrate`
166
- - sometimes, after repair or re-materialization work, you need **both**
167
-
168
- ### Authoring a later app-owned schema change
169
-
170
- When an existing CRUD-owned table needs a new column, constraint, index, or
171
- other compatible evolution, keep the generated baseline unchanged. Ask JSKIT
172
- to create a new migration source in the app-local package that owns the table:
12
+ For MySQL or MariaDB:
173
13
 
174
14
  ```bash
175
- npx jskit create migration \
176
- --package @local/workflow-record-report-values \
177
- --id extend-report-value-field-types
15
+ npm install @jskit-ai/database-runtime-mysql
178
16
  ```
179
17
 
180
- This command:
181
-
182
- 1. verifies that the owner is an installed app-local package
183
- 2. rejects duplicate or unsafe migration ids
184
- 3. creates an editable template under the package's
185
- `templates/migrations/` directory
186
- 4. adds the matching `install-migration` mutation to `package.json.jskit`
187
- 5. leaves the migration unmaterialized so its implementation can still be
188
- completed
189
-
190
- Implement and test the template first. It intentionally fails if someone tries
191
- to apply the untouched scaffold. Then materialize and apply it:
18
+ For PostgreSQL:
192
19
 
193
20
  ```bash
194
- npx jskit migrations sync
195
- npm run db:migrate
21
+ npm install @jskit-ai/database-runtime-postgres
196
22
  ```
197
23
 
198
- Once synchronized, the migration id and content are immutable. Any later correction
199
- must use another additive migration with a new id.
200
-
201
- SQL inside the source-controlled migration is supported when Knex does not
202
- express the required schema operation directly. Ad-hoc SQL applied only to a
203
- development or live database is not a migration and must not be used: it
204
- creates schema drift, breaks fresh reconstruction, and leaves deployment
205
- history incomplete.
206
-
207
- Before completion, exercise the complete migration chain against a fresh
208
- disposable database as well as the intended upgrade path. A down migration
209
- must refuse safely when narrowing the schema would invalidate existing data;
210
- it must never delete or silently transform valuable rows merely to make a
211
- rollback pass.
212
-
213
- ### Shared database helpers
214
-
215
- The database layer also gives later server code a shared helper surface:
216
-
217
- ```js
218
- import {
219
- applyVisibility,
220
- applyVisibilityOwners,
221
- toIsoString,
222
- toInsertDateTime,
223
- toNullableDateTime,
224
- isDuplicateEntryError,
225
- whereJsonTextEquals,
226
- createWithTransaction
227
- } from "@jskit-ai/database-runtime/shared";
228
- ```
229
-
230
- This is worth calling out here because the database layer is not only "Knex plus migrations".
231
-
232
- It also gives your later repositories and services a standard persistence toolbox so every package does not have to solve the same problems differently. The main point is consistency:
233
-
234
- - one way to format timestamps for database writes
235
- - one way to format timestamps for API output
236
- - one way to recognize duplicate-entry errors across databases
237
- - one way to apply ownership-aware visibility filters to queries
238
- - one way to stamp owner columns onto new rows from the current visibility context
239
- - one way to build some dialect-aware JSON filters
240
- - one small transaction helper pattern that packages can reuse
241
-
242
- That helper layer comes from `@jskit-ai/database-runtime/shared`. The MySQL package mainly contributes the actual driver and dialect registration. The shared helper surface comes from the generic runtime package.
243
-
244
- #### Visibility and ownership helpers
245
-
246
- This is the main helper pair that matters once repositories start dealing with owned records.
247
-
248
- In JSKIT persistence code, **visibility** means "which rows should this request be allowed to see or create?" It is the data-layer version of the same ownership model used by routes and CRUD resources:
249
-
250
- - `public`
251
- - the record is not scoped by owner columns
252
- - `workspace`
253
- - the record belongs to one workspace through the exact reserved column `workspace_id`
254
- - `user`
255
- - the record belongs to one user through the exact reserved column `user_id`
256
- - `workspace_user`
257
- - the record belongs to one workspace and one user through both reserved columns
258
-
259
- Only `workspace_id` and `user_id` carry this standard ownership contract. Specifically named foreign keys such as `recipient_user_id`, `created_by_user_id`, and `assignee_user_id` describe domain relationships; they are not alternate owner columns. Keep both fields when a row has an owner and a separate related actor, and never rename the relationship to an owner column merely to make a tool accept the schema.
260
-
261
- The selected ownership filter must match the direct reserved columns exactly. A table with only `workspace_id` is `workspace`; a table with only `user_id` is `user`; and a table with both is `workspace_user`. A declaration cannot override or ignore either column.
262
-
263
- That is why the shared helpers exist. Repositories should not have to re-implement the same ownership rules by hand every time they filter a query or build an insert payload.
264
-
265
- The expected input is a **visibility context**. In practice this usually arrives from route/action execution context and gets passed down as `queryOptions.visibilityContext` or `callOptions.visibilityContext`.
266
-
267
- The important shape looks like this:
268
-
269
- ```js
270
- const visibilityContext = {
271
- visibility: "workspace_user",
272
- scopeOwnerId: "42",
273
- userId: "7"
274
- };
275
- ```
276
-
277
- Those are the fields these helpers care about:
278
-
279
- - `visibility`
280
- - one of `public`, `workspace`, `user`, `workspace_user`
281
- - `scopeOwnerId`
282
- - the current workspace owner id
283
- - `userId`
284
- - the current user id
285
-
286
- There are a couple of extra normalized visibility fields elsewhere in JSKIT, but for `applyVisibility(...)` and `applyVisibilityOwners(...)`, those three values are the main contract.
287
-
288
- Use `applyVisibility(...)` when you are building a **read/update/delete query** that should only operate on rows visible to the current request.
289
-
290
- ```js
291
- import { applyVisibility } from "@jskit-ai/database-runtime/shared";
292
-
293
- function listWorkspaceContacts(knex, queryOptions = {}) {
294
- const query = knex("contacts").select("*").orderBy("created_at", "desc");
295
- return applyVisibility(query, queryOptions.visibilityContext);
296
- }
297
- ```
298
-
299
- If the current request is workspace-scoped:
300
-
301
- ```js
302
- const visibilityContext = {
303
- visibility: "workspace",
304
- scopeOwnerId: "12"
305
- };
306
- ```
307
-
308
- the helper turns that into the equivalent of:
309
-
310
- ```js
311
- query.where("workspace_id", "12");
312
- ```
313
-
314
- That is the normal helper for workspace-owned rows.
315
-
316
- The same pattern works for user-owned rows:
317
-
318
- ```js
319
- const query = knex("saved_views").select("*");
320
- applyVisibility(query, {
321
- visibility: "user",
322
- userId: "7"
323
- });
324
- ```
325
-
326
- which applies:
327
-
328
- ```js
329
- query.where("user_id", "7");
330
- ```
331
-
332
- And if a `workspace_user` context is required but only one owner id is available, the helper intentionally makes the query match nothing rather than accidentally leaking records. That is why these helpers are safer than scattering raw `where(...)` calls by hand.
333
-
334
- Use `applyVisibilityOwners(...)` when you are building a **write payload** for a new owned row and you want JSKIT to stamp the owner columns from the current visibility context.
335
-
336
- ```js
337
- import { applyVisibilityOwners, toInsertDateTime } from "@jskit-ai/database-runtime/shared";
338
-
339
- async function createWorkspaceNote(knex, payload, queryOptions = {}) {
340
- const insertPayload = applyVisibilityOwners(
341
- {
342
- title: payload.title,
343
- body: payload.body,
344
- created_at: toInsertDateTime(),
345
- updated_at: toInsertDateTime()
346
- },
347
- queryOptions.visibilityContext
348
- );
349
-
350
- await knex("notes").insert(insertPayload);
351
- }
352
- ```
353
-
354
- If the request is workspace-scoped, the helper adds `workspace_id` automatically. If it is user-scoped, it adds `user_id`. If it is `workspace_user`, it adds both. That means repository code does **not** need to manually copy `workspace_id` and `user_id` out of the execution context for every insert.
355
-
356
- That is especially useful for owned records such as:
357
-
358
- - workspace-owned rows like `contacts`, `workspace_invites`, or `assistant_config`
359
- - user-owned rows like personal settings or saved views
360
- - combined workspace-and-user rows where both ids define ownership
361
-
362
- So a `workspace_user` create can stay small:
363
-
364
- ```js
365
- const insertPayload = applyVisibilityOwners(
366
- {
367
- name: payload.name
368
- },
369
- {
370
- visibility: "workspace_user",
371
- scopeOwnerId: "4",
372
- userId: "9"
373
- }
374
- );
375
-
376
- // Result:
377
- // {
378
- // name: "...",
379
- // workspace_id: "4",
380
- // user_id: "9"
381
- // }
382
- ```
24
+ Each driver brings `@jskit-ai/database-runtime` and the appropriate Knex
25
+ driver through ordinary npm dependencies. There is no JSKIT install wizard.
383
26
 
384
- The important behavioral difference between the two helpers is:
27
+ ## Connection environment
385
28
 
386
- - `applyVisibility(...)`
387
- - scopes a query, and falls back to "no rows" when required owner ids are missing
388
- - `applyVisibilityOwners(...)`
389
- - stamps write payloads, and throws when required owner ids are missing
390
-
391
- That split is deliberate. For reads, "match nothing" is the safe default. For writes, silently inserting an incorrectly owned row would be worse, so the helper fails fast instead.
392
-
393
- If you want a slightly more structured repository shape, `createRepositoryScope(...)` from the same shared surface wraps these helpers into one small repository-scoping toolkit. But even when you write your own repositories manually, `applyVisibility(...)` and `applyVisibilityOwners(...)` are the key ownership helpers to reuse.
394
-
395
- #### Date helpers
396
-
397
- The date helpers are probably the first ones you are likely to reach for.
398
-
399
- They exist because application code usually deals with dates in two different shapes:
400
-
401
- - **database write shape**, such as a `DATETIME(3)`-style value
402
- - **API/read shape**, usually an ISO timestamp string
403
-
404
- Those are not the same thing, and it is easy for packages to become inconsistent if every repository hand-rolls the conversion.
405
-
406
- Typical usage looks like this:
407
-
408
- ```js
409
- import {
410
- toIsoString,
411
- toInsertDateTime,
412
- toNullableDateTime
413
- } from "@jskit-ai/database-runtime/shared";
414
-
415
- function mapRow(row) {
416
- return {
417
- id: String(row.id),
418
- createdAt: toIsoString(row.created_at),
419
- updatedAt: toIsoString(row.updated_at),
420
- expiresAt: row.expires_at ? toIsoString(row.expires_at) : null
421
- };
422
- }
423
-
424
- async function insertInvite(knex, payload) {
425
- await knex("workspace_invites").insert({
426
- email: payload.email,
427
- expires_at: toNullableDateTime(payload.expiresAt),
428
- created_at: toInsertDateTime(),
429
- updated_at: toInsertDateTime()
430
- });
431
- }
432
- ```
433
-
434
- The reasoning behind each helper is:
435
-
436
- - `toIsoString(...)`
437
- - use this when data is coming **out** of the database and you want a stable ISO timestamp for application code or API responses
438
- - `toInsertDateTime(...)`
439
- - use this when you need "now" in the database's datetime text format for insert/update timestamps
440
- - `toNullableDateTime(...)`
441
- - use this when a field is optional and should become either a normalized database datetime or `null`
442
-
443
- There is also a lower-level helper:
444
-
445
- ```js
446
- toDatabaseDateTimeUtc(value)
447
- ```
448
-
449
- That one is useful when you already know you are converting one specific date value into the database write format and you want to be explicit about it.
450
-
451
- The main benefit is not convenience by itself. The real benefit is that packages stop inventing slightly different timestamp formats and timezone assumptions.
452
-
453
- #### Duplicate-entry helpers
454
-
455
- Another practical problem is unique-constraint handling.
456
-
457
- MySQL and Postgres do not report duplicate-entry errors in exactly the same way. If every package checks raw driver error codes by hand, the code becomes repetitive and easy to get wrong.
458
-
459
- The shared helper gives you one check:
460
-
461
- ```js
462
- import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared";
463
-
464
- try {
465
- await knex("users").insert({
466
- email: payload.email
467
- });
468
- } catch (error) {
469
- if (!isDuplicateEntryError(error)) {
470
- throw error;
471
- }
472
-
473
- // Handle the "already exists" case here.
474
- }
475
- ```
476
-
477
- Why this is useful:
478
-
479
- - it keeps repository code cleaner
480
- - it avoids scattering vendor-specific numeric/string error codes everywhere
481
- - it makes later database portability less painful
482
-
483
- #### JSON query helpers
484
-
485
- Some databases expose different SQL syntax for reading JSON fields.
486
-
487
- The shared helpers smooth over a small but useful part of that difference. For example:
488
-
489
- ```js
490
- import { whereJsonTextEquals } from "@jskit-ai/database-runtime/shared";
491
-
492
- const query = knex("contacts");
493
- whereJsonTextEquals(query, {
494
- column: "metadata",
495
- path: "status",
496
- value: "active"
497
- });
498
- ```
499
-
500
- That helper uses the right JSON text expression for the active client instead of forcing every package to write separate MySQL and Postgres raw SQL.
501
-
502
- This is not a full ORM abstraction layer, and it should not be described that way. It is just a small set of helpers for the database differences JSKIT packages actually care about.
503
-
504
- #### Transaction and repository helpers
505
-
506
- The shared surface also includes a few small helpers for transaction-friendly repository code:
507
-
508
- ```js
509
- import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
510
-
511
- function createRepository(knex) {
512
- const withTransaction = createWithTransaction(knex);
513
-
514
- return {
515
- withTransaction
516
- };
517
- }
518
- ```
519
-
520
- That pattern shows up in JSKIT packages because it gives repositories a simple standard way to say, "run this unit of work inside a transaction".
521
-
522
- You do not need to memorize all of these helpers yet. The important thing to understand in this chapter is just that the database layer gives you:
523
-
524
- - runtime wiring
525
- - migration tooling
526
- - a shared persistence utility surface for later packages and app code
527
-
528
- ## What this still does not change yet
529
-
530
- Installing the database runtime is important, but it is not the same thing as installing the full users/account data model.
531
-
532
- Right now, after this chapter:
533
-
534
- - the app can resolve database settings from `.env`
535
- - the server can create a Knex client
536
- - the app can run migration commands
537
- - later packages are allowed to depend on `runtime.database`
538
-
539
- But the app still does **not** have:
540
-
541
- - JSKIT user tables
542
- - JSKIT user settings tables
543
- - persistent account/profile rows on the JSKIT side
544
- - workspace tables
545
- - CRUD tables of its own
546
-
547
- That means the app's account model still is not database-backed.
548
-
549
- - local auth is still the real source of truth for auth users and sessions.
550
- - JSKIT still has a database runtime available.
551
- - But JSKIT still has **no installed package yet** that projects auth identities into persistent users/account tables.
552
-
553
- So this chapter is an infrastructure step. It makes the database layer available, but it does not yet install the package that uses that layer for persistent JSKIT-side user data.
554
-
555
- **Important: Auth Is Not Users-Backed Yet**
556
-
557
- This is the most important thing to keep straight:
558
-
559
- - adding `database-runtime-mysql` does **not** automatically change where auth stores credentials or sessions
560
- - it also does **not** create JSKIT user rows yet
561
-
562
- That only changes later, when a package such as `users-core` is installed and registers the persistent users-backed `auth.profile.projector`.
563
-
564
- So after this chapter the app has a database layer, but authentication still behaves like:
565
-
566
- - real local auth
567
- - provider identity in the auth session
568
-
569
- not yet:
570
-
571
- - real local auth plus a persistent JSKIT-side users layer
572
-
573
- ## Under the hood
574
-
575
- The interesting files for this chapter are mostly at the top level:
576
-
577
- ```text
578
- exampleapp/
579
- .env
580
- knexfile.js
581
- migrations/
582
- .gitkeep
583
- package.json
584
- ```
585
-
586
- This is the first chapter where the new behavior is mostly about server infrastructure rather than pages or client layouts.
587
-
588
- ### `package.json` gains database dependencies and scripts
589
-
590
- After installing the MySQL runtime, the important new pieces in `package.json` look like this:
591
-
592
- ```json
593
- {
594
- "dependencies": {
595
- "@jskit-ai/database-runtime": "0.1.148",
596
- "@jskit-ai/database-runtime-mysql": "0.1.146",
597
- "knex": "^3.1.0",
598
- "mysql2": "^3.11.2"
599
- },
600
- "scripts": {
601
- "db:migrations:sync": "jskit migrations sync",
602
- "db:migrate": "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:latest",
603
- "db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
604
- "db:migrate:status": "npm run db:migrations:sync && knex --knexfile ./knexfile.js migrate:list"
605
- }
606
- }
607
- ```
608
-
609
- Those new dependencies divide into two roles:
610
-
611
- - `@jskit-ai/database-runtime` is the generic JSKIT database runtime
612
- - `@jskit-ai/database-runtime-mysql` is the MySQL-specific driver package
613
- - `knex` is the database toolkit used by both runtime code and migration commands
614
- - `mysql2` is the actual Node driver that speaks to MySQL
615
-
616
- The migration scripts are also worth reading carefully:
617
-
618
- - `db:migrations:sync` writes missing immutable package migration files in `migrations/`
619
- - `db:migrate` syncs JSKIT-managed migration files, then applies all pending Knex migrations
620
- - `db:migrate:rollback` rolls back the last migration batch
621
- - `db:migrate:status` syncs JSKIT-managed migration files, then lists applied and pending migrations
622
-
623
- They are not special JSKIT commands. They are ordinary project scripts, which makes them easy to run in any environment.
624
-
625
- ### `.env` owns the database connection settings
626
-
627
- The package install also writes the database settings into `.env`:
29
+ Keep credentials outside Git. Supply either `DATABASE_URL` or the individual
30
+ values:
628
31
 
629
32
  ```dotenv
630
- DB_CLIENT=mysql2
631
33
  DB_HOST=127.0.0.1
632
34
  DB_PORT=3306
633
35
  DB_NAME=exampleapp
@@ -635,209 +37,127 @@ DB_USER=exampleapp
635
37
  DB_PASSWORD=secret
636
38
  ```
637
39
 
638
- That small block is doing two jobs.
639
-
640
- - `DB_CLIENT` tells the generic database runtime which dialect was installed.
641
- - the rest of the variables describe the real connection to MySQL.
40
+ Use port `5432` for PostgreSQL. A single-driver app fixes its dialect in
41
+ `knexfile.js`, so it does not need `DB_CLIENT`. The running database provider
42
+ also derives its dialect from the installed driver and rejects a conflicting
43
+ `DB_CLIENT` value when one is supplied.
642
44
 
643
- This matters because the generic runtime is written to support more than one driver package. The runtime does not hard-code MySQL. It reads the configured client and the installed driver and checks that they agree.
45
+ The application or hosting environment creates the database and provides its
46
+ credentials. JSKIT never guesses or commits them.
644
47
 
645
- ### `knexfile.js` is for migration commands, not normal page code
48
+ ## Migration configuration
646
49
 
647
- The migration scripts in `package.json` work because the app has a top-level `knexfile.js`:
50
+ Use the fixed-dialect pattern owned by the selected driver. The important
51
+ application file is small:
648
52
 
649
53
  ```js
650
- import path from "node:path";
651
- import dotenv from "dotenv";
652
- import {
653
- normalizeText,
654
- toKnexClientId,
655
- resolveDatabaseClientFromEnvironment,
656
- resolveKnexConnectionFromEnvironment
657
- } from "@jskit-ai/database-runtime/shared";
658
-
659
- const appRoot = process.cwd();
660
- dotenv.config({
661
- path: path.join(appRoot, ".env"),
662
- quiet: true
663
- });
664
-
665
- const dialectId = resolveDatabaseClientFromEnvironment(process.env);
666
- const client = toKnexClientId(dialectId);
667
- const defaultPort = dialectId === "pg" ? 5432 : 3306;
668
- const migrationsDirectory = path.resolve(appRoot, normalizeText(process.env.DB_MIGRATIONS_DIR) || "migrations");
669
- const deferredConstraintsDirectory = path.join(migrationsDirectory, "constraints");
670
-
671
- export default {
672
- client,
673
- connection: resolveKnexConnectionFromEnvironment(process.env, {
674
- client: dialectId,
675
- defaultPort,
676
- context: "knex migrations"
677
- }),
678
- migrations: {
679
- directory: [migrationsDirectory, deferredConstraintsDirectory],
680
- extension: "cjs",
681
- sortDirsSeparately: true
682
- }
683
- };
684
- ```
685
-
686
- The important thing to understand is what this file is **for**.
54
+ import { createKnexMigrationConfigFromApp } from
55
+ "@jskit-ai/database-runtime/server/knexMigrationConfig";
687
56
 
688
- It is not the main runtime API that your app code imports during a request. It is the configuration file the Knex CLI reads when you run commands such as:
689
-
690
- ```bash
691
- npm run db:migrate
57
+ export default await createKnexMigrationConfigFromApp({ client: "mysql2" });
692
58
  ```
693
59
 
694
- So there are really two separate database entry points:
695
-
696
- - `knexfile.js` for migration commands
697
- - the JSKIT server provider runtime for application code
698
-
699
- That separation is good. It keeps the operational CLI workflow and the app runtime wiring clear.
60
+ Use `client: "pg"` for PostgreSQL. The complete examples are in:
700
61
 
701
- The two migration directories are one ordered migration plan. Knex completes
702
- the ordinary `migrations/` files before it reads
703
- `migrations/constraints/`. CRUD scaffolding uses that second phase for foreign
704
- keys, so two tables may validly reference one another without either
705
- table-creation migration depending on a table that has not been created yet.
706
- Rollback reverses the order and removes those constraints before dropping
707
- tables.
62
+ - `database/mysql-application`
63
+ - `database/postgres-application`
708
64
 
709
- ### The MySQL package registers the driver, and the generic runtime builds the Knex client
65
+ The config discovers migrations directly from the current installed package
66
+ graph. Application migrations live in the app's `migrations/` directory;
67
+ package-owned migrations live in directories declared by the installed
68
+ package's `package.json#jskit.migrations.directories`.
710
69
 
711
- On the server side, the two installed packages split responsibilities very deliberately.
70
+ There is no migration sync command and no copied migration projection. Knex
71
+ runs the authoritative files where their owners ship them.
712
72
 
713
- The MySQL-specific package registers a driver token:
73
+ ## What runs migrations
714
74
 
715
- ```js
716
- class DatabaseRuntimeMysqlServiceProvider {
717
- static id = "runtime.database.driver.mysql";
75
+ The application owns normal npm scripts:
718
76
 
719
- register(app) {
720
- app.singleton("runtime.database.driver.mysql", () => MYSQL_DATABASE_DRIVER_API);
77
+ ```json
78
+ {
79
+ "scripts": {
80
+ "db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
81
+ "db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
82
+ "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
721
83
  }
722
84
  }
723
85
  ```
724
86
 
725
- That does **not** create the database client yet. It only tells the app, "a MySQL driver is available, and here is its dialect metadata."
87
+ `npm run db:migrate` runs the Knex CLI. Knex loads `knexfile.js`, discovers the
88
+ application and installed-package migration directories, connects using the
89
+ environment, and applies pending migrations.
726
90
 
727
- The generic runtime then uses that driver to create the real Knex wiring:
91
+ Deployment or a managed development environment may invoke this app-owned
92
+ script as a release step. JSKIT itself does not maintain a background migration
93
+ service.
728
94
 
729
- ```js
730
- class DatabaseRuntimeServiceProvider {
731
- static id = "runtime.database";
732
-
733
- register(app) {
734
- app.singleton("runtime.database", () => DATABASE_RUNTIME_SERVER_API);
95
+ ## Authoring schema changes
735
96
 
736
- if (!app.has("runtime.database.driver")) {
737
- app.singleton("runtime.database.driver", (scope) => resolveSingleRegisteredDriver(scope));
738
- }
97
+ Write a new immutable migration in the package that owns the schema. Never edit
98
+ an already-applied migration and never change a live database without recording
99
+ the equivalent source-controlled migration.
739
100
 
740
- if (!app.has("jskit.database.knex")) {
741
- app.singleton("jskit.database.knex", (scope) => createKnexInstance(scope));
742
- }
101
+ Package migrations are normal `.cjs` Knex migrations and are declared in that
102
+ package's metadata:
743
103
 
744
- if (!app.has("jskit.database.transactionManager")) {
745
- app.singleton("jskit.database.transactionManager", (scope) => {
746
- const knex = scope.make("jskit.database.knex");
747
- return createTransactionManager({ knex });
748
- });
104
+ ```json
105
+ {
106
+ "jskit": {
107
+ "migrations": {
108
+ "directories": ["migrations"]
749
109
  }
750
110
  }
751
111
  }
752
112
  ```
753
113
 
754
- That one provider is the real center of this chapter. It gives later server code a standard set of container tokens:
755
-
756
- - `runtime.database`
757
- - `runtime.database.driver`
758
- - `jskit.database.knex`
759
- - `jskit.database.transactionManager`
760
-
761
- This is why later packages can simply say "I require `runtime.database`" instead of building their own database bootstrap.
762
-
763
- ### Why the browser still feels unchanged
764
-
765
- At first glance it can feel strange that the database layer is installed but the app still behaves almost exactly like the previous chapter.
766
-
767
- The reason is simple:
768
-
769
- - the runtime is available
770
- - but almost no installed package is using it yet
771
-
772
- Right now:
773
-
774
- - `shell-web` is still a shell/layout package
775
- - `auth-web` is still a web auth package
776
- - `auth-provider-local-core` is still handling credentials and sessions through `auth.local.backend`
777
-
778
- So the app has gained a new capability, but no visible part of the UI depends on that capability yet.
779
-
780
- ### Why auth is not users-backed yet
781
-
782
- This is the most important code path to read in this chapter.
783
-
784
- Inside the local provider, auth only projects provider identities into the app users layer when something registers the provider-neutral `auth.profile.projector` token:
785
-
786
- ```js
787
- const profileProjector = scope.has("auth.profile.projector")
788
- ? {
789
- async syncIdentityProfile(profile, options = {}) {
790
- const projector = scope.make("auth.profile.projector");
791
- return projector.syncIdentityProfile(profile, options);
792
- }
793
- }
794
- : null;
795
- ```
796
-
797
- That snippet explains the whole consequence of this chapter. The provider checks whether the token exists, but it does not resolve the real projector until an auth payload actually needs profile projection.
798
-
799
- - The auth provider can authenticate users without an app database.
800
- - Nothing in `database-runtime-mysql` registers `auth.profile.projector`.
801
- - Nothing in `database-runtime-mysql` provides `users.profile.sync.service`.
802
- - The database runtime only provides the database foundation that later packages can use.
803
-
804
- So the auth layer keeps behaving the same way it did before:
805
-
806
- - local auth still owns the auth user and session
807
- - JSKIT can still display the provider identity from the auth session
808
- - there is still no persistent JSKIT users/account model yet
809
-
810
- The database runtime is ready, but the users layer that will actually use it has not been installed yet.
811
-
812
- ### Why the empty `migrations/` directory is important
813
-
814
- The new `migrations/` directory can look almost silly at first because it only contains `.gitkeep`. But that empty directory is the cleanest signal of what this chapter really does.
815
-
816
- It means:
817
-
818
- - the app has a migration system
819
- - the app does **not** yet have a schema of its own
820
-
821
- That is exactly the right state at this stage of the guide.
822
-
823
- The database runtime chapter should give the app a database foundation first. The next data-heavy chapters can then install actual schema migrations on top of that foundation.
824
-
825
- ## Summary
826
-
827
- This chapter did not make the app feel dramatically different in the browser, but it changed the server foundation in an important way.
828
-
829
- - the app has a real JSKIT database runtime
830
- - the app has a standard Knex migration workflow
831
- - the app has a place for future schema files
832
-
833
- But just as importantly, this chapter also defined what has **not** changed yet:
834
-
835
- - auth still uses the local provider's own backend
836
- - JSKIT still has no persistent users layer of its own
837
- - no feature package has started storing real app data yet
838
-
839
- So the right mental model at the end of this chapter is:
840
-
841
- - local auth already handles real authentication
842
- - MySQL is wired up and ready
843
- - the persistent JSKIT-side user model arrives in the next chapter
114
+ Names must remain unique across the effective migration directories. Keep
115
+ constraints in a later migration when ordering matters.
116
+
117
+ ## Seed data is not a migration
118
+
119
+ Migrations establish schema and invariant framework data. Product fixtures,
120
+ sample accounts, catalog content, and other environment-specific starting data
121
+ belong in an explicit, idempotent application seed operation that runs after
122
+ migrations. Do not hide product seeding in schema migrations.
123
+
124
+ Managed editors must be able to create one isolated database per development
125
+ session, apply the full migration graph, then invoke that explicit seed
126
+ operation. JSKIT supplies portable migration and seed seams; the editor owns
127
+ database allocation, credentials, lifetime, and environment injection.
128
+
129
+ ## Resource services and custom operations
130
+
131
+ Conventional persisted resources use `defineCrudResource()` and
132
+ `defineCrudJsonApiFeature()` so the framework owns repeated repository,
133
+ service, action, permission, JSON API, and route mechanics. This does not make
134
+ product CRUD behavior fixed.
135
+
136
+ - `decorateRepository` adds resource-specific queries, locks, or writes.
137
+ - `decorateService` overrides a standard method or adds domain methods such as
138
+ `confirm`, `publish`, `cancel`, or `sendReminder`.
139
+ - `operationLifecycle` surrounds a standard operation with `before`, `execute`,
140
+ `after`, and mutation-only `afterCommit` phases. Create, update, and delete
141
+ phases before commit share one repository transaction, and `execute` receives
142
+ `standard(nextInput)` for retaining the normal framework write.
143
+ - Named `actions` expose non-CRUD service methods through normal input,
144
+ permission, audit, event, and optional HTTP route contracts.
145
+
146
+ Repositories own database access. Services and lifecycle hooks orchestrate
147
+ repositories. External delivery belongs after commit; when it must be durable,
148
+ write an outbox record inside the transaction and deliver it separately.
149
+ A separate Feature is warranted when an operation belongs to another domain,
150
+ not merely because a useful resource has behavior beyond list and save.
151
+
152
+ ## Verification
153
+
154
+ - Rebuild a disposable database from the complete migration graph.
155
+ - Run `npm run db:migrate:status` after migration.
156
+ - Exercise a real transaction and one invalid-connection case.
157
+ - When a seed operation exists, run it twice and require the second run to be
158
+ safe.
159
+ - Test MySQL and PostgreSQL patterns independently.
160
+
161
+ Do not add migration receipts, sync ledgers, generator provenance, or dialect
162
+ questionnaires. The installed graph, migration source, environment, and
163
+ database migration table are sufficient.