@jskit-ai/agent-docs 0.1.1
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/DISTR_AGENT.md +25 -0
- package/guide/agent/app-extras/assistant.md +636 -0
- package/guide/agent/app-extras/realtime.md +223 -0
- package/guide/agent/app-setup/a-more-interesting-shell.md +643 -0
- package/guide/agent/app-setup/authentication.md +948 -0
- package/guide/agent/app-setup/console.md +316 -0
- package/guide/agent/app-setup/database-layer.md +775 -0
- package/guide/agent/app-setup/initial-scaffolding.md +714 -0
- package/guide/agent/app-setup/multi-homing.md +655 -0
- package/guide/agent/app-setup/users.md +355 -0
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +983 -0
- package/guide/agent/generators/advanced-cruds.md +923 -0
- package/guide/agent/generators/crud-generators.md +556 -0
- package/guide/agent/generators/intro.md +63 -0
- package/guide/agent/generators/ui-generators.md +648 -0
- package/guide/agent/index.md +39 -0
- package/guide/human/app-extras/assistant.md +695 -0
- package/guide/human/app-extras/realtime.md +270 -0
- package/guide/human/app-setup/a-more-interesting-shell.md +734 -0
- package/guide/human/app-setup/authentication.md +963 -0
- package/guide/human/app-setup/console.md +352 -0
- package/guide/human/app-setup/database-layer.md +822 -0
- package/guide/human/app-setup/initial-scaffolding.md +738 -0
- package/guide/human/app-setup/multi-homing.md +795 -0
- package/guide/human/app-setup/users.md +404 -0
- package/guide/human/app-setup/working-with-the-jskit-cli.md +997 -0
- package/guide/human/generators/advanced-cruds.md +923 -0
- package/guide/human/generators/crud-generators.md +556 -0
- package/guide/human/generators/intro.md +109 -0
- package/guide/human/generators/ui-generators.md +665 -0
- package/guide/human/index.md +39 -0
- package/package.json +28 -0
- package/reference/autogen/KERNEL_MAP.md +536 -0
- package/reference/autogen/README.md +44 -0
- package/reference/autogen/packages/agent-docs.md +13 -0
- package/reference/autogen/packages/assistant-core.md +310 -0
- package/reference/autogen/packages/assistant-runtime.md +219 -0
- package/reference/autogen/packages/assistant.md +73 -0
- package/reference/autogen/packages/auth-core.md +352 -0
- package/reference/autogen/packages/auth-provider-supabase-core.md +223 -0
- package/reference/autogen/packages/auth-web.md +267 -0
- package/reference/autogen/packages/console-core.md +116 -0
- package/reference/autogen/packages/console-web.md +37 -0
- package/reference/autogen/packages/crud-core.md +283 -0
- package/reference/autogen/packages/crud-server-generator.md +220 -0
- package/reference/autogen/packages/crud-ui-generator.md +154 -0
- package/reference/autogen/packages/database-runtime-mysql.md +61 -0
- package/reference/autogen/packages/database-runtime-postgres.md +39 -0
- package/reference/autogen/packages/database-runtime.md +216 -0
- package/reference/autogen/packages/http-runtime.md +213 -0
- package/reference/autogen/packages/kernel.md +1350 -0
- package/reference/autogen/packages/realtime.md +95 -0
- package/reference/autogen/packages/shell-web.md +349 -0
- package/reference/autogen/packages/storage-runtime.md +39 -0
- package/reference/autogen/packages/ui-generator.md +101 -0
- package/reference/autogen/packages/uploads-image-web.md +76 -0
- package/reference/autogen/packages/uploads-runtime.md +85 -0
- package/reference/autogen/packages/users-core.md +307 -0
- package/reference/autogen/packages/users-web.md +473 -0
- package/reference/autogen/packages/workspaces-core.md +415 -0
- package/reference/autogen/packages/workspaces-web.md +372 -0
- package/reference/autogen/tooling/config-eslint.md +52 -0
- package/reference/autogen/tooling/create-app.md +194 -0
- package/reference/autogen/tooling/jskit-catalog.md +27 -0
- package/reference/autogen/tooling/jskit-cli.md +624 -0
- package/reference/autogen/tooling/test-support.md +27 -0
- package/reference/autogen/tooling/testUtils.md +31 -0
- package/templates/APP_BLUEPRINT.md +57 -0
- package/workflow/app-state.md +33 -0
- package/workflow/bootstrap.md +24 -0
- package/workflow/feature-delivery.md +21 -0
- package/workflow/review.md +22 -0
- package/workflow/scoping.md +26 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
<!-- Generated by `npm run agent-docs:build` from `docs/guide/app-setup/database-layer.md`. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
# Database layer
|
|
4
|
+
|
|
5
|
+
At the end of the previous chapter, the app could already authenticate real users through Supabase, but JSKIT was still using its no-database fallback for the app-side user mirror. 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.
|
|
6
|
+
|
|
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.
|
|
8
|
+
|
|
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
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The first command adds the MySQL driver package and its generic JSKIT database runtime dependency, using the explicit connection values from those `DB_*` variables:
|
|
30
|
+
|
|
31
|
+
- database host
|
|
32
|
+
- database port
|
|
33
|
+
- database name
|
|
34
|
+
- database user
|
|
35
|
+
- database password
|
|
36
|
+
|
|
37
|
+
The second command downloads the newly referenced runtime packages and the underlying Node dependencies, especially `knex` and `mysql2`.
|
|
38
|
+
|
|
39
|
+
If you open the app in the browser after this install, it still looks the same. That is expected.
|
|
40
|
+
|
|
41
|
+
- `/home` still renders the same shell
|
|
42
|
+
- `/auth/login` still renders the same login screen
|
|
43
|
+
|
|
44
|
+
This chapter changes the server-side infrastructure of the app, not the visible shell.
|
|
45
|
+
|
|
46
|
+
## What changes immediately
|
|
47
|
+
|
|
48
|
+
Installing `database-runtime-mysql` gives the app three important things right away.
|
|
49
|
+
|
|
50
|
+
### A real database runtime
|
|
51
|
+
|
|
52
|
+
The server can now 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.
|
|
53
|
+
|
|
54
|
+
### A migration workflow
|
|
55
|
+
|
|
56
|
+
The app now gets three new scripts in `package.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"scripts": {
|
|
61
|
+
"db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
|
|
62
|
+
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
63
|
+
"db:migrate:status": "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 now, 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
|
+
There are now two different migration-related layers in the app:
|
|
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
|
+
### `jskit migrations ...` writes managed migration files
|
|
99
|
+
|
|
100
|
+
If you run:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx jskit migrations changed
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
JSKIT checks the installed package state in `.jskit/lock.json` and materializes any managed migration files that need to exist in `migrations/`.
|
|
107
|
+
|
|
108
|
+
That command is about the app scaffold on disk.
|
|
109
|
+
|
|
110
|
+
It does **not** run Knex against MySQL or Postgres.
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
### `npm run db:migrate` runs Knex against the database
|
|
115
|
+
|
|
116
|
+
When you run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run db:migrate
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
the Knex CLI reads `knexfile.js`, connects to the real database, and applies any pending migration files it finds.
|
|
123
|
+
|
|
124
|
+
That command is about the actual database.
|
|
125
|
+
|
|
126
|
+
So the clean mental model is:
|
|
127
|
+
|
|
128
|
+
1. JSKIT writes or refreshes managed migration files into `migrations/`
|
|
129
|
+
2. Knex applies those files to MySQL or Postgres
|
|
130
|
+
|
|
131
|
+
### When you need each step
|
|
132
|
+
|
|
133
|
+
In many normal `jskit add package ...` flows, JSKIT already materializes a package's managed migration files while the package is being applied.
|
|
134
|
+
|
|
135
|
+
That means the most common flow is still:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx jskit add package users-web
|
|
139
|
+
npm install
|
|
140
|
+
npm run db:migrate
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
But if you ever need JSKIT to re-materialize or refresh the managed migration files for installed packages, that is when you use:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npx jskit migrations changed
|
|
147
|
+
npm run db:migrate
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
So:
|
|
151
|
+
|
|
152
|
+
- use `jskit migrations ...` when you need JSKIT to write or refresh managed migration files
|
|
153
|
+
- use `npm run db:migrate` when you need Knex to apply pending migration files to the real database
|
|
154
|
+
- sometimes you need only `npm run db:migrate`
|
|
155
|
+
- sometimes, after repair or re-materialization work, you need **both**
|
|
156
|
+
|
|
157
|
+
### Shared database helpers
|
|
158
|
+
|
|
159
|
+
The database layer also gives later server code a shared helper surface:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import {
|
|
163
|
+
applyVisibility,
|
|
164
|
+
applyVisibilityOwners,
|
|
165
|
+
toIsoString,
|
|
166
|
+
toInsertDateTime,
|
|
167
|
+
toNullableDateTime,
|
|
168
|
+
isDuplicateEntryError,
|
|
169
|
+
whereJsonTextEquals,
|
|
170
|
+
createWithTransaction
|
|
171
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This is worth calling out now because the database layer is not only "Knex plus migrations".
|
|
175
|
+
|
|
176
|
+
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:
|
|
177
|
+
|
|
178
|
+
- one way to format timestamps for database writes
|
|
179
|
+
- one way to format timestamps for API output
|
|
180
|
+
- one way to recognize duplicate-entry errors across databases
|
|
181
|
+
- one way to apply ownership-aware visibility filters to queries
|
|
182
|
+
- one way to stamp owner columns onto new rows from the current visibility context
|
|
183
|
+
- one way to build some dialect-aware JSON filters
|
|
184
|
+
- one small transaction helper pattern that packages can reuse
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
#### Visibility and ownership helpers
|
|
189
|
+
|
|
190
|
+
This is the main helper pair that matters once repositories start dealing with owned records.
|
|
191
|
+
|
|
192
|
+
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:
|
|
193
|
+
|
|
194
|
+
- `public`
|
|
195
|
+
- the record is not scoped by owner columns
|
|
196
|
+
- `workspace`
|
|
197
|
+
- the record belongs to one workspace, usually through `workspace_id`
|
|
198
|
+
- `user`
|
|
199
|
+
- the record belongs to one user, usually through `user_id`
|
|
200
|
+
- `workspace_user`
|
|
201
|
+
- the record belongs to one workspace and one user together
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
|
|
205
|
+
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`.
|
|
206
|
+
|
|
207
|
+
The important shape looks like this:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
const visibilityContext = {
|
|
211
|
+
visibility: "workspace_user",
|
|
212
|
+
scopeOwnerId: "42",
|
|
213
|
+
userId: "7"
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Those are the fields these helpers care about:
|
|
218
|
+
|
|
219
|
+
- `visibility`
|
|
220
|
+
- one of `public`, `workspace`, `user`, `workspace_user`
|
|
221
|
+
- `scopeOwnerId`
|
|
222
|
+
- the current workspace owner id
|
|
223
|
+
- `userId`
|
|
224
|
+
- the current user id
|
|
225
|
+
|
|
226
|
+
There are a couple of extra normalized visibility fields elsewhere in JSKIT, but for `applyVisibility(...)` and `applyVisibilityOwners(...)`, those three values are the main contract.
|
|
227
|
+
|
|
228
|
+
Use `applyVisibility(...)` when you are building a **read/update/delete query** that should only operate on rows visible to the current request.
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { applyVisibility } from "@jskit-ai/database-runtime/shared";
|
|
232
|
+
|
|
233
|
+
function listWorkspaceContacts(knex, queryOptions = {}) {
|
|
234
|
+
const query = knex("contacts").select("*").orderBy("created_at", "desc");
|
|
235
|
+
return applyVisibility(query, queryOptions.visibilityContext);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
If the current request is workspace-scoped:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
const visibilityContext = {
|
|
243
|
+
visibility: "workspace",
|
|
244
|
+
scopeOwnerId: "12"
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
the helper turns that into the equivalent of:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
query.where("workspace_id", "12");
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
That is the normal helper for workspace-owned rows.
|
|
255
|
+
|
|
256
|
+
The same pattern works for user-owned rows:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
const query = knex("saved_views").select("*");
|
|
260
|
+
applyVisibility(query, {
|
|
261
|
+
visibility: "user",
|
|
262
|
+
userId: "7"
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
which applies:
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
query.where("user_id", "7");
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
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.
|
|
273
|
+
|
|
274
|
+
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.
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
import { applyVisibilityOwners, toInsertDateTime } from "@jskit-ai/database-runtime/shared";
|
|
278
|
+
|
|
279
|
+
async function createWorkspaceNote(knex, payload, queryOptions = {}) {
|
|
280
|
+
const insertPayload = applyVisibilityOwners(
|
|
281
|
+
{
|
|
282
|
+
title: payload.title,
|
|
283
|
+
body: payload.body,
|
|
284
|
+
created_at: toInsertDateTime(),
|
|
285
|
+
updated_at: toInsertDateTime()
|
|
286
|
+
},
|
|
287
|
+
queryOptions.visibilityContext
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
await knex("notes").insert(insertPayload);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
That is especially useful for owned records such as:
|
|
297
|
+
|
|
298
|
+
- workspace-owned rows like `contacts`, `workspace_invites`, or `assistant_config`
|
|
299
|
+
- user-owned rows like personal settings or saved views
|
|
300
|
+
- combined workspace-and-user rows where both ids define ownership
|
|
301
|
+
|
|
302
|
+
So a `workspace_user` create can stay small:
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
const insertPayload = applyVisibilityOwners(
|
|
306
|
+
{
|
|
307
|
+
name: payload.name
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
visibility: "workspace_user",
|
|
311
|
+
scopeOwnerId: "4",
|
|
312
|
+
userId: "9"
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Result:
|
|
317
|
+
// {
|
|
318
|
+
// name: "...",
|
|
319
|
+
// workspace_id: "4",
|
|
320
|
+
// user_id: "9"
|
|
321
|
+
// }
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The important behavioral difference between the two helpers is:
|
|
325
|
+
|
|
326
|
+
- `applyVisibility(...)`
|
|
327
|
+
- scopes a query, and falls back to "no rows" when required owner ids are missing
|
|
328
|
+
- `applyVisibilityOwners(...)`
|
|
329
|
+
- stamps write payloads, and throws when required owner ids are missing
|
|
330
|
+
|
|
331
|
+
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.
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
#### Date helpers
|
|
336
|
+
|
|
337
|
+
The date helpers are probably the first ones you are likely to reach for.
|
|
338
|
+
|
|
339
|
+
They exist because application code usually deals with dates in two different shapes:
|
|
340
|
+
|
|
341
|
+
- **database write shape**, such as a `DATETIME(3)`-style value
|
|
342
|
+
- **API/read shape**, usually an ISO timestamp string
|
|
343
|
+
|
|
344
|
+
Those are not the same thing, and it is easy for packages to become inconsistent if every repository hand-rolls the conversion.
|
|
345
|
+
|
|
346
|
+
Typical usage looks like this:
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
import {
|
|
350
|
+
toIsoString,
|
|
351
|
+
toInsertDateTime,
|
|
352
|
+
toNullableDateTime
|
|
353
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
354
|
+
|
|
355
|
+
function mapRow(row) {
|
|
356
|
+
return {
|
|
357
|
+
id: String(row.id),
|
|
358
|
+
createdAt: toIsoString(row.created_at),
|
|
359
|
+
updatedAt: toIsoString(row.updated_at),
|
|
360
|
+
expiresAt: row.expires_at ? toIsoString(row.expires_at) : null
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function insertInvite(knex, payload) {
|
|
365
|
+
await knex("workspace_invites").insert({
|
|
366
|
+
email: payload.email,
|
|
367
|
+
expires_at: toNullableDateTime(payload.expiresAt),
|
|
368
|
+
created_at: toInsertDateTime(),
|
|
369
|
+
updated_at: toInsertDateTime()
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The reasoning behind each helper is:
|
|
375
|
+
|
|
376
|
+
- `toIsoString(...)`
|
|
377
|
+
- use this when data is coming **out** of the database and you want a stable ISO timestamp for application code or API responses
|
|
378
|
+
- `toInsertDateTime(...)`
|
|
379
|
+
- use this when you need "now" in the database's datetime text format for insert/update timestamps
|
|
380
|
+
- `toNullableDateTime(...)`
|
|
381
|
+
- use this when a field is optional and should become either a normalized database datetime or `null`
|
|
382
|
+
|
|
383
|
+
There is also a lower-level helper:
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
toDatabaseDateTimeUtc(value)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
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.
|
|
390
|
+
|
|
391
|
+
The main benefit is not convenience by itself. The real benefit is that packages stop inventing slightly different timestamp formats and timezone assumptions.
|
|
392
|
+
|
|
393
|
+
#### Duplicate-entry helpers
|
|
394
|
+
|
|
395
|
+
Another practical problem is unique-constraint handling.
|
|
396
|
+
|
|
397
|
+
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.
|
|
398
|
+
|
|
399
|
+
The shared helper gives you one check:
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared";
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await knex("users").insert({
|
|
406
|
+
email: payload.email
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (!isDuplicateEntryError(error)) {
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Handle the "already exists" case here.
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Why this is useful:
|
|
418
|
+
|
|
419
|
+
- it keeps repository code cleaner
|
|
420
|
+
- it avoids scattering vendor-specific numeric/string error codes everywhere
|
|
421
|
+
- it makes later database portability less painful
|
|
422
|
+
|
|
423
|
+
#### JSON query helpers
|
|
424
|
+
|
|
425
|
+
Some databases expose different SQL syntax for reading JSON fields.
|
|
426
|
+
|
|
427
|
+
The shared helpers smooth over a small but useful part of that difference. For example:
|
|
428
|
+
|
|
429
|
+
```js
|
|
430
|
+
import { whereJsonTextEquals } from "@jskit-ai/database-runtime/shared";
|
|
431
|
+
|
|
432
|
+
const query = knex("contacts");
|
|
433
|
+
whereJsonTextEquals(query, {
|
|
434
|
+
column: "metadata",
|
|
435
|
+
path: "status",
|
|
436
|
+
value: "active"
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
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.
|
|
441
|
+
|
|
442
|
+
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.
|
|
443
|
+
|
|
444
|
+
#### Transaction and repository helpers
|
|
445
|
+
|
|
446
|
+
The shared surface also includes a few small helpers for transaction-friendly repository code:
|
|
447
|
+
|
|
448
|
+
```js
|
|
449
|
+
import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
|
|
450
|
+
|
|
451
|
+
function createRepository(knex) {
|
|
452
|
+
const withTransaction = createWithTransaction(knex);
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
withTransaction
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
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".
|
|
461
|
+
|
|
462
|
+
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:
|
|
463
|
+
|
|
464
|
+
- runtime wiring
|
|
465
|
+
- migration tooling
|
|
466
|
+
- a shared persistence utility surface for later packages and app code
|
|
467
|
+
|
|
468
|
+
## What this still does not change yet
|
|
469
|
+
|
|
470
|
+
Installing the database runtime is important, but it is not the same thing as installing the full users/account data model.
|
|
471
|
+
|
|
472
|
+
Right now, after this chapter:
|
|
473
|
+
|
|
474
|
+
- the app can resolve database settings from `.env`
|
|
475
|
+
- the server can create a Knex client
|
|
476
|
+
- the app can run migration commands
|
|
477
|
+
- later packages are allowed to depend on `runtime.database`
|
|
478
|
+
|
|
479
|
+
But the app still does **not** have:
|
|
480
|
+
|
|
481
|
+
- JSKIT user tables
|
|
482
|
+
- JSKIT user settings tables
|
|
483
|
+
- persistent account/profile rows on the JSKIT side
|
|
484
|
+
- workspace tables
|
|
485
|
+
- CRUD tables of its own
|
|
486
|
+
|
|
487
|
+
That means authentication is still only **partly** database-backed.
|
|
488
|
+
|
|
489
|
+
- Supabase is still the real source of truth for auth users and sessions.
|
|
490
|
+
- JSKIT still has a database runtime available.
|
|
491
|
+
- But JSKIT still has **no installed package yet** that tells auth to switch from the standalone in-memory profile mirror to the persistent users-backed one.
|
|
492
|
+
|
|
493
|
+
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.
|
|
494
|
+
|
|
495
|
+
**Important: Auth Is Still In Standalone Mode**
|
|
496
|
+
|
|
497
|
+
This is the most important thing to keep straight:
|
|
498
|
+
|
|
499
|
+
- adding `database-runtime-mysql` does **not** automatically make the auth chapter's temporary mirror disappear
|
|
500
|
+
- it also does **not** create JSKIT user rows yet
|
|
501
|
+
|
|
502
|
+
That only changes later, when a package such as `users-core` is installed and tells auth to use the persistent users-backed profile sync mode.
|
|
503
|
+
|
|
504
|
+
So after this chapter the app has a database layer, but authentication still behaves like:
|
|
505
|
+
|
|
506
|
+
- real Supabase auth
|
|
507
|
+
- temporary JSKIT-side mirror
|
|
508
|
+
|
|
509
|
+
not yet:
|
|
510
|
+
|
|
511
|
+
- real Supabase auth
|
|
512
|
+
- persistent JSKIT-side users layer
|
|
513
|
+
|
|
514
|
+
## Under the hood
|
|
515
|
+
|
|
516
|
+
The interesting files for this chapter are mostly at the top level:
|
|
517
|
+
|
|
518
|
+
```text
|
|
519
|
+
exampleapp/
|
|
520
|
+
.env
|
|
521
|
+
knexfile.js
|
|
522
|
+
migrations/
|
|
523
|
+
.gitkeep
|
|
524
|
+
package.json
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
This is the first chapter where the new behavior is mostly about server infrastructure rather than pages or client layouts.
|
|
528
|
+
|
|
529
|
+
### `package.json` gains database dependencies and scripts
|
|
530
|
+
|
|
531
|
+
After installing the MySQL runtime, the important new pieces in `package.json` look like this:
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{
|
|
535
|
+
"dependencies": {
|
|
536
|
+
"@jskit-ai/database-runtime": "0.x",
|
|
537
|
+
"@jskit-ai/database-runtime-mysql": "0.x",
|
|
538
|
+
"knex": "^3.1.0",
|
|
539
|
+
"mysql2": "^3.11.2"
|
|
540
|
+
},
|
|
541
|
+
"scripts": {
|
|
542
|
+
"db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
|
|
543
|
+
"db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
|
|
544
|
+
"db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Those new dependencies divide into two roles:
|
|
550
|
+
|
|
551
|
+
- `@jskit-ai/database-runtime` is the generic JSKIT database runtime
|
|
552
|
+
- `@jskit-ai/database-runtime-mysql` is the MySQL-specific driver package
|
|
553
|
+
- `knex` is the database toolkit used by both runtime code and migration commands
|
|
554
|
+
- `mysql2` is the actual Node driver that speaks to MySQL
|
|
555
|
+
|
|
556
|
+
The three new scripts are also worth reading carefully:
|
|
557
|
+
|
|
558
|
+
- `db:migrate` applies all pending migrations
|
|
559
|
+
- `db:migrate:rollback` rolls back the last migration batch
|
|
560
|
+
- `db:migrate:status` lists applied and pending migrations
|
|
561
|
+
|
|
562
|
+
They are not special JSKIT commands. They are ordinary project scripts, which makes them easy to run in any environment.
|
|
563
|
+
|
|
564
|
+
### `.env` now owns the database connection settings
|
|
565
|
+
|
|
566
|
+
The package install also writes the database settings into `.env`:
|
|
567
|
+
|
|
568
|
+
```dotenv
|
|
569
|
+
DB_CLIENT=mysql2
|
|
570
|
+
DB_HOST=127.0.0.1
|
|
571
|
+
DB_PORT=3306
|
|
572
|
+
DB_NAME=exampleapp
|
|
573
|
+
DB_USER=exampleapp
|
|
574
|
+
DB_PASSWORD=secret
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
That small block is doing two jobs.
|
|
578
|
+
|
|
579
|
+
- `DB_CLIENT` tells the generic database runtime which dialect was installed.
|
|
580
|
+
- the rest of the variables describe the real connection to MySQL.
|
|
581
|
+
|
|
582
|
+
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.
|
|
583
|
+
|
|
584
|
+
### `knexfile.js` is for migration commands, not normal page code
|
|
585
|
+
|
|
586
|
+
The migration scripts in `package.json` work because the app now has a top-level `knexfile.js`:
|
|
587
|
+
|
|
588
|
+
```js
|
|
589
|
+
import path from "node:path";
|
|
590
|
+
import dotenv from "dotenv";
|
|
591
|
+
import {
|
|
592
|
+
normalizeText,
|
|
593
|
+
toKnexClientId,
|
|
594
|
+
resolveDatabaseClientFromEnvironment,
|
|
595
|
+
resolveKnexConnectionFromEnvironment
|
|
596
|
+
} from "@jskit-ai/database-runtime/shared";
|
|
597
|
+
|
|
598
|
+
const appRoot = process.cwd();
|
|
599
|
+
dotenv.config({
|
|
600
|
+
path: path.join(appRoot, ".env"),
|
|
601
|
+
quiet: true
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const dialectId = resolveDatabaseClientFromEnvironment(process.env);
|
|
605
|
+
const client = toKnexClientId(dialectId);
|
|
606
|
+
const defaultPort = dialectId === "pg" ? 5432 : 3306;
|
|
607
|
+
const migrationsDirectory = path.resolve(appRoot, normalizeText(process.env.DB_MIGRATIONS_DIR) || "migrations");
|
|
608
|
+
|
|
609
|
+
export default {
|
|
610
|
+
client,
|
|
611
|
+
connection: resolveKnexConnectionFromEnvironment(process.env, {
|
|
612
|
+
client: dialectId,
|
|
613
|
+
defaultPort,
|
|
614
|
+
context: "knex migrations"
|
|
615
|
+
}),
|
|
616
|
+
migrations: {
|
|
617
|
+
directory: migrationsDirectory,
|
|
618
|
+
extension: "cjs"
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
The important thing to understand is what this file is **for**.
|
|
624
|
+
|
|
625
|
+
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:
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
npm run db:migrate
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
So there are really two separate database entry points now:
|
|
632
|
+
|
|
633
|
+
- `knexfile.js` for migration commands
|
|
634
|
+
- the JSKIT server provider runtime for application code
|
|
635
|
+
|
|
636
|
+
That separation is good. It keeps the operational CLI workflow and the app runtime wiring clear.
|
|
637
|
+
|
|
638
|
+
### The MySQL package registers the driver, and the generic runtime builds the Knex client
|
|
639
|
+
|
|
640
|
+
On the server side, the two installed packages split responsibilities very deliberately.
|
|
641
|
+
|
|
642
|
+
The MySQL-specific package registers a driver token:
|
|
643
|
+
|
|
644
|
+
```js
|
|
645
|
+
class DatabaseRuntimeMysqlServiceProvider {
|
|
646
|
+
static id = "runtime.database.driver.mysql";
|
|
647
|
+
|
|
648
|
+
register(app) {
|
|
649
|
+
app.singleton("runtime.database.driver.mysql", () => MYSQL_DATABASE_DRIVER_API);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
That does **not** create the database client yet. It only tells the app, "a MySQL driver is available, and here is its dialect metadata."
|
|
655
|
+
|
|
656
|
+
The generic runtime then uses that driver to create the real Knex wiring:
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
class DatabaseRuntimeServiceProvider {
|
|
660
|
+
static id = "runtime.database";
|
|
661
|
+
|
|
662
|
+
register(app) {
|
|
663
|
+
app.singleton("runtime.database", () => DATABASE_RUNTIME_SERVER_API);
|
|
664
|
+
|
|
665
|
+
if (!app.has("runtime.database.driver")) {
|
|
666
|
+
app.singleton("runtime.database.driver", (scope) => resolveSingleRegisteredDriver(scope));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!app.has("jskit.database.knex")) {
|
|
670
|
+
app.singleton("jskit.database.knex", (scope) => createKnexInstance(scope));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!app.has("jskit.database.transactionManager")) {
|
|
674
|
+
app.singleton("jskit.database.transactionManager", (scope) => {
|
|
675
|
+
const knex = scope.make("jskit.database.knex");
|
|
676
|
+
return createTransactionManager({ knex });
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
That one provider is the real center of this chapter. It gives later server code a standard set of container tokens:
|
|
684
|
+
|
|
685
|
+
- `runtime.database`
|
|
686
|
+
- `runtime.database.driver`
|
|
687
|
+
- `jskit.database.knex`
|
|
688
|
+
- `jskit.database.transactionManager`
|
|
689
|
+
|
|
690
|
+
This is why later packages can simply say "I require `runtime.database`" instead of building their own database bootstrap.
|
|
691
|
+
|
|
692
|
+
### Why the browser still feels unchanged
|
|
693
|
+
|
|
694
|
+
At first glance it can feel strange that the database layer is installed but the app still behaves almost exactly like the previous chapter.
|
|
695
|
+
|
|
696
|
+
The reason is simple:
|
|
697
|
+
|
|
698
|
+
- the runtime is now available
|
|
699
|
+
- but almost no installed package is using it yet
|
|
700
|
+
|
|
701
|
+
Right now:
|
|
702
|
+
|
|
703
|
+
- `shell-web` is still a shell/layout package
|
|
704
|
+
- `auth-web` is still a web auth package
|
|
705
|
+
- `auth-provider-supabase-core` is still talking to Supabase for the real auth work
|
|
706
|
+
|
|
707
|
+
So the app has gained a new capability, but no visible part of the UI depends on that capability yet.
|
|
708
|
+
|
|
709
|
+
### Why auth still uses the standalone profile mirror
|
|
710
|
+
|
|
711
|
+
This is the most important code path to read in this chapter.
|
|
712
|
+
|
|
713
|
+
Inside `AuthSupabaseServiceProvider`, auth still resolves its profile mode from the environment:
|
|
714
|
+
|
|
715
|
+
```js
|
|
716
|
+
const authProfileMode = resolveAuthProfileMode(env);
|
|
717
|
+
let userProfileSyncService = fallbackStandaloneProfileSyncService;
|
|
718
|
+
|
|
719
|
+
if (authProfileMode === AUTH_PROFILE_MODE_USERS) {
|
|
720
|
+
if (!scope.has("users.profile.sync.service")) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
"AuthSupabaseServiceProvider requires users.profile.sync.service when AUTH_PROFILE_MODE=users."
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
userProfileSyncService = scope.make("users.profile.sync.service");
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
That snippet explains the whole consequence of this chapter.
|
|
730
|
+
|
|
731
|
+
- The default mode is still `standalone`.
|
|
732
|
+
- The fallback service is still the in-memory profile sync service from the previous chapter.
|
|
733
|
+
- Nothing in `database-runtime-mysql` changes `AUTH_PROFILE_MODE`.
|
|
734
|
+
- Nothing in `database-runtime-mysql` provides `users.profile.sync.service`.
|
|
735
|
+
|
|
736
|
+
So the auth layer keeps behaving the same way it did before:
|
|
737
|
+
|
|
738
|
+
- Supabase still owns the real auth user and session
|
|
739
|
+
- JSKIT still mirrors just enough profile data locally
|
|
740
|
+
- that JSKIT-side mirror is still not persistent yet
|
|
741
|
+
|
|
742
|
+
The database runtime is now ready, but the users layer that will actually use it has not been installed yet.
|
|
743
|
+
|
|
744
|
+
### Why the empty `migrations/` directory is important
|
|
745
|
+
|
|
746
|
+
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.
|
|
747
|
+
|
|
748
|
+
It means:
|
|
749
|
+
|
|
750
|
+
- the app now has a migration system
|
|
751
|
+
- the app does **not** yet have a schema of its own
|
|
752
|
+
|
|
753
|
+
That is exactly the right state at this stage of the guide.
|
|
754
|
+
|
|
755
|
+
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.
|
|
756
|
+
|
|
757
|
+
## Summary
|
|
758
|
+
|
|
759
|
+
This chapter did not make the app feel dramatically different in the browser, but it changed the server foundation in an important way.
|
|
760
|
+
|
|
761
|
+
- the app now has a real JSKIT database runtime
|
|
762
|
+
- the app now has a standard Knex migration workflow
|
|
763
|
+
- the app now has a place for future schema files
|
|
764
|
+
|
|
765
|
+
But just as importantly, this chapter also defined what has **not** changed yet:
|
|
766
|
+
|
|
767
|
+
- auth still uses the standalone JSKIT-side mirror
|
|
768
|
+
- JSKIT still has no persistent users layer of its own
|
|
769
|
+
- no feature package has started storing real app data yet
|
|
770
|
+
|
|
771
|
+
So the right mental model at the end of this chapter is:
|
|
772
|
+
|
|
773
|
+
- Supabase already handles real authentication
|
|
774
|
+
- MySQL is now wired up and ready
|
|
775
|
+
- the persistent JSKIT-side user model arrives in the next chapter
|