@danieljvdm/dev-kit 0.11.3 → 0.12.0
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/README.md +70 -76
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/dev-kit/SKILL.md +58 -46
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +49 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +0 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +16 -14
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +170 -120
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
|
@@ -1,536 +0,0 @@
|
|
|
1
|
-
# SQL Guide
|
|
2
|
-
|
|
3
|
-
This guide covers Effect SQL. Apply the persistence authority, adapter
|
|
4
|
-
ownership, dependency visibility, Schema, and honest test-layer policies from
|
|
5
|
-
the focused local guides as well.
|
|
6
|
-
|
|
7
|
-
Key source files:
|
|
8
|
-
|
|
9
|
-
- `packages/effect/src/unstable/sql/SqlClient.ts`
|
|
10
|
-
- `packages/effect/src/unstable/sql/Migrator.ts`
|
|
11
|
-
- `packages/effect/src/unstable/sql/SqlResolver.ts`
|
|
12
|
-
- `packages/effect/src/unstable/sql/SqlSchema.ts`
|
|
13
|
-
- `packages/effect/src/unstable/sql/SqlModel.ts`
|
|
14
|
-
- `packages/effect/src/unstable/sql/SqlError.ts`
|
|
15
|
-
|
|
16
|
-
## Preferred Rule
|
|
17
|
-
|
|
18
|
-
When a project uses Effect, prefer the Effect SQL modules over directly coupling business code to a native SQL driver API.
|
|
19
|
-
|
|
20
|
-
Prefer:
|
|
21
|
-
|
|
22
|
-
- `effect/unstable/sql/SqlClient`
|
|
23
|
-
- `effect/unstable/sql/Migrator`
|
|
24
|
-
- `effect/unstable/sql/SqlResolver`
|
|
25
|
-
- `effect/unstable/sql/SqlSchema`
|
|
26
|
-
- `effect/unstable/sql/SqlModel`
|
|
27
|
-
|
|
28
|
-
Over:
|
|
29
|
-
|
|
30
|
-
- embedding raw driver calls directly in business services
|
|
31
|
-
- hand-rolling transactions in service methods
|
|
32
|
-
- ad hoc migration scripts disconnected from the Effect runtime
|
|
33
|
-
|
|
34
|
-
Why:
|
|
35
|
-
|
|
36
|
-
- transactions are integrated into the Effect model
|
|
37
|
-
- spans and SQL observability are built in
|
|
38
|
-
- SQL errors stay typed and consistent
|
|
39
|
-
- schema decoding and request resolution compose better
|
|
40
|
-
- layers and services stay portable across runtimes and tests
|
|
41
|
-
|
|
42
|
-
## Mental Model
|
|
43
|
-
|
|
44
|
-
The Effect SQL stack is organized around:
|
|
45
|
-
|
|
46
|
-
- `SqlClient` as the main database capability
|
|
47
|
-
- `withTransaction` for transaction boundaries
|
|
48
|
-
- `SqlResolver` for request-style batched and validated access
|
|
49
|
-
- `SqlSchema` and `SqlModel` for schema-aware query/model patterns
|
|
50
|
-
- `Migrator` for managed migrations
|
|
51
|
-
|
|
52
|
-
The business layer should depend on Effect SQL abstractions, not on a raw driver object.
|
|
53
|
-
|
|
54
|
-
## `SqlClient`
|
|
55
|
-
|
|
56
|
-
`SqlClient` is the primary service for executing SQL.
|
|
57
|
-
|
|
58
|
-
Repo reference:
|
|
59
|
-
|
|
60
|
-
- `packages/effect/src/unstable/sql/SqlClient.ts`
|
|
61
|
-
|
|
62
|
-
Use it when:
|
|
63
|
-
|
|
64
|
-
- you need to execute queries
|
|
65
|
-
- you need transactions
|
|
66
|
-
- you want SQL operations to participate in Effect spans and context
|
|
67
|
-
|
|
68
|
-
Important capabilities from the repo:
|
|
69
|
-
|
|
70
|
-
- transaction support with `withTransaction`
|
|
71
|
-
- connection reservation with `reserve`
|
|
72
|
-
- reactive queries
|
|
73
|
-
- integration with transaction-scoped context
|
|
74
|
-
|
|
75
|
-
## Typed Queries
|
|
76
|
-
|
|
77
|
-
Prefer typed SQL queries instead of leaving row shapes implicit.
|
|
78
|
-
|
|
79
|
-
The repo uses typed query literals like:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
const rows = yield * sql<{ id: number; name: string }>`SELECT * FROM test`;
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
This is the first level of typed SQL usage and is already better than untyped row access.
|
|
86
|
-
|
|
87
|
-
Use typed query literals when:
|
|
88
|
-
|
|
89
|
-
- the row shape is small and obvious
|
|
90
|
-
- the query is local and does not justify a reusable schema
|
|
91
|
-
- you want immediate row typing without introducing extra helpers
|
|
92
|
-
|
|
93
|
-
Avoid:
|
|
94
|
-
|
|
95
|
-
- leaving query results untyped and then recovering shape with unsafe assertions
|
|
96
|
-
- using `as` on rows after query execution
|
|
97
|
-
|
|
98
|
-
## Schema Integration
|
|
99
|
-
|
|
100
|
-
Prefer integrating SQL with Schema whenever the row shape matters or the query result crosses a meaningful boundary.
|
|
101
|
-
|
|
102
|
-
Why:
|
|
103
|
-
|
|
104
|
-
- Schema validates the shape rather than trusting the database blindly
|
|
105
|
-
- row decoding stays explicit and typed
|
|
106
|
-
- the same schema can often be reused for transport, domain, or contract layers
|
|
107
|
-
- this avoids unsafe row assertions such as `as TodoRow`
|
|
108
|
-
|
|
109
|
-
### Prefer schema-decoded results over `as`-based rows
|
|
110
|
-
|
|
111
|
-
Avoid this pattern:
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
const row = yield * sql`SELECT id, title FROM todos WHERE id = ${id}`;
|
|
115
|
-
const todo = row[0] as TodoRow;
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Prefer:
|
|
119
|
-
|
|
120
|
-
- a typed SQL query plus Schema decoding
|
|
121
|
-
- or a SQL helper such as `SqlResolver`, `SqlSchema`, or `SqlModel` when appropriate
|
|
122
|
-
|
|
123
|
-
Example boundary decode:
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
const TodoRow = Schema.Struct({
|
|
127
|
-
id: Schema.Number,
|
|
128
|
-
title: Schema.String,
|
|
129
|
-
completed: Schema.Boolean,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const decodeTodoRows = Schema.decodeUnknownEffect(Schema.Array(TodoRow));
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Then keep the query and decoding together in one SQL-aware operation.
|
|
136
|
-
|
|
137
|
-
### `SqlResolver` + Schema
|
|
138
|
-
|
|
139
|
-
`SqlResolver` is one of the clearest examples of SQL and Schema integration in the canonical source.
|
|
140
|
-
|
|
141
|
-
It uses:
|
|
142
|
-
|
|
143
|
-
- a `Request` schema for validating resolver input
|
|
144
|
-
- a `Result` schema for validating query output
|
|
145
|
-
|
|
146
|
-
Example shape from the repo tests:
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
const resolver = SqlResolver.findById({
|
|
150
|
-
Id: Schema.Number,
|
|
151
|
-
Result: Schema.Struct({
|
|
152
|
-
id: Schema.Number,
|
|
153
|
-
name: Schema.String,
|
|
154
|
-
}),
|
|
155
|
-
ResultId: (row) => row.id,
|
|
156
|
-
execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}`,
|
|
157
|
-
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
This is a preferred pattern when:
|
|
161
|
-
|
|
162
|
-
- multiple IDs are fetched together
|
|
163
|
-
- request batching is useful
|
|
164
|
-
- the query contract should be schema-validated
|
|
165
|
-
|
|
166
|
-
### `SqlSchema` and `SqlModel`
|
|
167
|
-
|
|
168
|
-
Use `SqlSchema` and `SqlModel` when you want tighter schema integration with SQL itself.
|
|
169
|
-
|
|
170
|
-
They are preferred over hand-written row types when:
|
|
171
|
-
|
|
172
|
-
- the row shape is central to the module
|
|
173
|
-
- you want reusable schema-aware model logic
|
|
174
|
-
- manual row mapping is becoming repetitive
|
|
175
|
-
|
|
176
|
-
## Prefer SQL Services Over Native Driver Services
|
|
177
|
-
|
|
178
|
-
Avoid this pattern:
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
class TodoService extends Context.Service<TodoService, { ... }>()("TodoService") {
|
|
182
|
-
static readonly layer = Layer.effect(this)(
|
|
183
|
-
Effect.acquireRelease(
|
|
184
|
-
Effect.try({ try: () => new Database("todos.sqlite") })
|
|
185
|
-
)
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Why this is usually a bad pattern in an Effect codebase:
|
|
191
|
-
|
|
192
|
-
- the service is now tightly coupled to one runtime-specific database client
|
|
193
|
-
- you lose the shared SQL abstraction that the Effect repo already provides
|
|
194
|
-
- transaction and query conventions become ad hoc
|
|
195
|
-
- it is easier to drift away from typed SQL errors, SQL tracing, and reusable query helpers
|
|
196
|
-
|
|
197
|
-
Prefer a layer that provides `SqlClient`, and let domain services depend on that.
|
|
198
|
-
|
|
199
|
-
## Domain Services Should Depend On `SqlClient`
|
|
200
|
-
|
|
201
|
-
Good pattern:
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import * as Context from "effect/Context";
|
|
205
|
-
import * as Effect from "effect/Effect";
|
|
206
|
-
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
207
|
-
|
|
208
|
-
class TodoRepo extends Context.Service<TodoRepo>()("TodoRepo", {
|
|
209
|
-
make: Effect.succeed({
|
|
210
|
-
getById: Effect.fn("TodoRepo.getById")(function* (id: number) {
|
|
211
|
-
const sql = yield* SqlClient.SqlClient;
|
|
212
|
-
return yield* sql`SELECT id, title, completed FROM todos WHERE id = ${id}`;
|
|
213
|
-
}),
|
|
214
|
-
}),
|
|
215
|
-
}) {}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Why this is better:
|
|
219
|
-
|
|
220
|
-
- the domain service depends on an Effect SQL capability
|
|
221
|
-
- SQL stays observable and transactional
|
|
222
|
-
- the SQL client implementation can be provided separately from the business service
|
|
223
|
-
|
|
224
|
-
## Transactions
|
|
225
|
-
|
|
226
|
-
Yield `SqlClient.SqlClient`, then use the service's `sql.withTransaction`
|
|
227
|
-
method for transaction boundaries.
|
|
228
|
-
|
|
229
|
-
Repo reference:
|
|
230
|
-
|
|
231
|
-
- `packages/effect/src/unstable/sql/SqlClient.ts`
|
|
232
|
-
|
|
233
|
-
Prefer:
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
const createAndAudit = Effect.fn("TodoRepo.createAndAudit")(function* (title: string) {
|
|
237
|
-
const sql = yield* SqlClient.SqlClient;
|
|
238
|
-
|
|
239
|
-
return yield* sql.withTransaction(
|
|
240
|
-
Effect.gen(function* () {
|
|
241
|
-
yield* sql`INSERT INTO todos ${sql.insert({ title, completed: false })}`;
|
|
242
|
-
yield* sql`INSERT INTO audit_log ${sql.insert({ event: "todo_created" })}`;
|
|
243
|
-
}),
|
|
244
|
-
);
|
|
245
|
-
});
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
Avoid:
|
|
249
|
-
|
|
250
|
-
- manual `BEGIN` / `COMMIT` / `ROLLBACK` in application service code
|
|
251
|
-
- driver-specific transaction logic spread across multiple service methods
|
|
252
|
-
|
|
253
|
-
## Query Composition
|
|
254
|
-
|
|
255
|
-
Prefer keeping query logic inside SQL-aware services or repositories.
|
|
256
|
-
|
|
257
|
-
Good patterns:
|
|
258
|
-
|
|
259
|
-
- keep queries close to the service that owns the behavior
|
|
260
|
-
- use named business operations for multi-step workflows
|
|
261
|
-
- use small local helpers only when they improve clarity
|
|
262
|
-
|
|
263
|
-
Avoid exposing one exported accessor function per SQL service method if it only forwards to the service.
|
|
264
|
-
|
|
265
|
-
Bad:
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
export const createTodo = Effect.fn(function* (title: string) {
|
|
269
|
-
const todos = yield* TodoRepo;
|
|
270
|
-
return yield* todos.create(title);
|
|
271
|
-
});
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
Prefer:
|
|
275
|
-
|
|
276
|
-
- use the service method directly within the owning workflow
|
|
277
|
-
- or export a real business operation that adds behavior beyond simple forwarding
|
|
278
|
-
|
|
279
|
-
## SQL Resolvers
|
|
280
|
-
|
|
281
|
-
Use `SqlResolver` when request-style batching or schema-validated request/response handling is useful.
|
|
282
|
-
|
|
283
|
-
Repo reference:
|
|
284
|
-
|
|
285
|
-
- `packages/effect/src/unstable/sql/SqlResolver.ts`
|
|
286
|
-
|
|
287
|
-
It is especially good for:
|
|
288
|
-
|
|
289
|
-
- batched lookup patterns
|
|
290
|
-
- `findById`-style resolvers
|
|
291
|
-
- grouped query resolution
|
|
292
|
-
- integrating SQL with request batching patterns
|
|
293
|
-
|
|
294
|
-
Important repo pattern:
|
|
295
|
-
|
|
296
|
-
- request schema validates inputs
|
|
297
|
-
- result schema validates outputs
|
|
298
|
-
- execution remains effectful and transactional
|
|
299
|
-
|
|
300
|
-
This is one of the strongest typed-query patterns in the repo and should be preferred over ad hoc batched row mapping when the query fits the resolver model.
|
|
301
|
-
|
|
302
|
-
## SQL Schemas And Models
|
|
303
|
-
|
|
304
|
-
When schema-aware SQL helpers fit the task, prefer them over hand-mapped rows.
|
|
305
|
-
|
|
306
|
-
Look at:
|
|
307
|
-
|
|
308
|
-
- `SqlSchema`
|
|
309
|
-
- `SqlModel`
|
|
310
|
-
|
|
311
|
-
These are good fits when:
|
|
312
|
-
|
|
313
|
-
- the row shape matters strongly
|
|
314
|
-
- schema-based decode/encode should stay aligned with database access
|
|
315
|
-
- you want to reduce ad hoc row mapping logic
|
|
316
|
-
|
|
317
|
-
Avoid overusing manual `type Row = { ... }` plus custom conversion if the schema-aware modules already express the shape clearly.
|
|
318
|
-
|
|
319
|
-
Preferred order for query typing:
|
|
320
|
-
|
|
321
|
-
1. schema-aware SQL module such as `SqlResolver`, `SqlSchema`, or `SqlModel` when it fits
|
|
322
|
-
2. typed query literals plus Schema decoding when the query is local
|
|
323
|
-
3. only use manual row mapping when the first two options are clearly heavier than the problem
|
|
324
|
-
|
|
325
|
-
## Migrations
|
|
326
|
-
|
|
327
|
-
Use `Migrator` for migrations.
|
|
328
|
-
|
|
329
|
-
Repo reference:
|
|
330
|
-
|
|
331
|
-
- `packages/effect/src/unstable/sql/Migrator.ts`
|
|
332
|
-
|
|
333
|
-
The repo shows these best practices:
|
|
334
|
-
|
|
335
|
-
- maintain a dedicated migrations table
|
|
336
|
-
- load migrations through a managed loader
|
|
337
|
-
- run migrations through the Effect runtime
|
|
338
|
-
- keep migration execution observable with logs and spans
|
|
339
|
-
- use SQL client transaction and locking semantics handled by the migrator
|
|
340
|
-
|
|
341
|
-
Important behavior in the canonical source:
|
|
342
|
-
|
|
343
|
-
- migrations table creation is dialect-aware
|
|
344
|
-
- duplicate migration IDs are detected
|
|
345
|
-
- concurrent migration runs are guarded
|
|
346
|
-
- each migration is logged and wrapped in a span
|
|
347
|
-
|
|
348
|
-
### Concrete Migration Loader Example
|
|
349
|
-
|
|
350
|
-
The runtime-specific SQL migrator packages expose `fromRecord(...)` to define migrations from an ordered record.
|
|
351
|
-
|
|
352
|
-
Example shape:
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
import * as SqliteMigrator from "@effect/sql-sqlite-bun/SqliteMigrator";
|
|
356
|
-
import * as Effect from "effect/Effect";
|
|
357
|
-
|
|
358
|
-
const migrations = SqliteMigrator.fromRecord({
|
|
359
|
-
"1_create_todos": Effect.gen(function* () {
|
|
360
|
-
yield* sql`
|
|
361
|
-
CREATE TABLE todos (
|
|
362
|
-
id INTEGER PRIMARY KEY NOT NULL,
|
|
363
|
-
title TEXT NOT NULL,
|
|
364
|
-
completed INTEGER NOT NULL DEFAULT 0
|
|
365
|
-
)
|
|
366
|
-
`.withoutTransform;
|
|
367
|
-
}),
|
|
368
|
-
"2_add_todo_index": Effect.gen(function* () {
|
|
369
|
-
yield* sql`
|
|
370
|
-
CREATE INDEX todos_completed_idx ON todos (completed)
|
|
371
|
-
`.withoutTransform;
|
|
372
|
-
}),
|
|
373
|
-
});
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
This matches the model used by the canonical migrator implementation:
|
|
377
|
-
|
|
378
|
-
- each migration has a numeric prefix and descriptive name
|
|
379
|
-
- each migration resolves to an `Effect`
|
|
380
|
-
- migrations are ordered by ID
|
|
381
|
-
|
|
382
|
-
### Running Migrations Directly
|
|
383
|
-
|
|
384
|
-
Use the runtime-specific `run(...)` helper when you want a startup effect that runs migrations explicitly.
|
|
385
|
-
|
|
386
|
-
Example shape:
|
|
387
|
-
|
|
388
|
-
```ts
|
|
389
|
-
import * as SqliteMigrator from "@effect/sql-sqlite-bun/SqliteMigrator";
|
|
390
|
-
|
|
391
|
-
const runMigrations = SqliteMigrator.run({
|
|
392
|
-
loader: migrations,
|
|
393
|
-
});
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
This is a good fit when:
|
|
397
|
-
|
|
398
|
-
- startup explicitly runs migrations before launching the main app
|
|
399
|
-
- deployment tooling runs migrations as a separate command
|
|
400
|
-
- you want migration results as an ordinary `Effect`
|
|
401
|
-
|
|
402
|
-
The return value includes the applied migration IDs and names.
|
|
403
|
-
|
|
404
|
-
### Running Migrations As A Layer
|
|
405
|
-
|
|
406
|
-
Use the runtime-specific `layer(...)` helper when migrations should run as part of top-level infrastructure setup.
|
|
407
|
-
|
|
408
|
-
Example shape:
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
import * as SqliteMigrator from "@effect/sql-sqlite-bun/SqliteMigrator";
|
|
412
|
-
|
|
413
|
-
const MigrationLayer = SqliteMigrator.layer({
|
|
414
|
-
loader: migrations,
|
|
415
|
-
});
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
From the canonical packages, this is implemented as `Layer.effectDiscard(run(options))`.
|
|
419
|
-
|
|
420
|
-
That means:
|
|
421
|
-
|
|
422
|
-
- the layer performs the migration effect
|
|
423
|
-
- it does not provide a new service of its own
|
|
424
|
-
- it is intended to be composed into startup infrastructure
|
|
425
|
-
|
|
426
|
-
### Concrete Startup Composition Example
|
|
427
|
-
|
|
428
|
-
Preferred shape:
|
|
429
|
-
|
|
430
|
-
```ts
|
|
431
|
-
const SqlLayer = SqliteClient.layer({ filename: "todos.sqlite" });
|
|
432
|
-
|
|
433
|
-
const MigrationLayer = SqliteMigrator.layer({
|
|
434
|
-
loader: migrations,
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
const MigratedSqlLayer = Layer.merge(SqlLayer, MigrationLayer.pipe(Layer.provide(SqlLayer)));
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
This keeps the structure explicit:
|
|
441
|
-
|
|
442
|
-
- one layer provides the SQL client
|
|
443
|
-
- one layer runs migrations
|
|
444
|
-
- the merged layer represents a migrated SQL environment
|
|
445
|
-
|
|
446
|
-
If the same migrated SQL environment is reused in tests, create it once and reuse the layer value.
|
|
447
|
-
|
|
448
|
-
### Migration Best Practices
|
|
449
|
-
|
|
450
|
-
- use stable numeric migration IDs
|
|
451
|
-
- keep migration files ordered and unique
|
|
452
|
-
- do not hand-roll a separate migrations subsystem if `Migrator` already fits the project
|
|
453
|
-
- keep migration execution at startup or a dedicated operational boundary
|
|
454
|
-
- do not bury migration execution inside arbitrary service construction unless startup is explicitly the right place
|
|
455
|
-
|
|
456
|
-
### Preferred Migration Boundary
|
|
457
|
-
|
|
458
|
-
Good pattern:
|
|
459
|
-
|
|
460
|
-
- construct the SQL layer
|
|
461
|
-
- run migrations once at startup or deployment entry
|
|
462
|
-
- then run the main application
|
|
463
|
-
|
|
464
|
-
Concrete shapes:
|
|
465
|
-
|
|
466
|
-
- separate startup effect: `SqliteMigrator.run({ loader })`
|
|
467
|
-
- startup layer: `SqliteMigrator.layer({ loader })`
|
|
468
|
-
- migrated environment layer: merge the SQL client layer with the migration layer provided by that client layer
|
|
469
|
-
|
|
470
|
-
Avoid:
|
|
471
|
-
|
|
472
|
-
- opportunistic migrations inside ordinary request handlers
|
|
473
|
-
- schema creation hidden inside unrelated business service constructors
|
|
474
|
-
|
|
475
|
-
## Errors
|
|
476
|
-
|
|
477
|
-
Prefer SQL errors that stay inside the Effect SQL model as long as possible.
|
|
478
|
-
|
|
479
|
-
Use domain-level translation only where it helps the business boundary.
|
|
480
|
-
|
|
481
|
-
Good:
|
|
482
|
-
|
|
483
|
-
- SQL layer or repository works with `SqlError` and schema decode failures where appropriate
|
|
484
|
-
- higher-level service translates expected cases into domain errors when needed
|
|
485
|
-
|
|
486
|
-
Avoid:
|
|
487
|
-
|
|
488
|
-
- converting every SQL error immediately into a string
|
|
489
|
-
- hiding SQL failure details too early
|
|
490
|
-
|
|
491
|
-
## Observability
|
|
492
|
-
|
|
493
|
-
The canonical SQL modules already integrate with spans and transaction context.
|
|
494
|
-
|
|
495
|
-
This is another reason to prefer them over raw driver usage.
|
|
496
|
-
|
|
497
|
-
Good pattern:
|
|
498
|
-
|
|
499
|
-
- use `SqlClient` and `withTransaction`
|
|
500
|
-
- keep business operations wrapped with `Effect.fn`
|
|
501
|
-
- add explicit spans only where business-level detail matters beyond the built-in SQL spans
|
|
502
|
-
|
|
503
|
-
## Layering Pattern
|
|
504
|
-
|
|
505
|
-
Preferred layering shape:
|
|
506
|
-
|
|
507
|
-
1. runtime-specific database layer provides `SqlClient`
|
|
508
|
-
2. migrations run at startup boundary
|
|
509
|
-
3. domain repository/service depends on `SqlClient`
|
|
510
|
-
4. top-level application layer composes the database layer with the business layers
|
|
511
|
-
|
|
512
|
-
This keeps:
|
|
513
|
-
|
|
514
|
-
- driver choice at the edge
|
|
515
|
-
- SQL capability in the middle
|
|
516
|
-
- business logic above it
|
|
517
|
-
|
|
518
|
-
## Anti-Patterns
|
|
519
|
-
|
|
520
|
-
- embedding a native driver directly in business services when Effect SQL modules are available
|
|
521
|
-
- hand-rolling transactions with raw SQL statements in service methods
|
|
522
|
-
- hiding schema creation inside unrelated service constructors
|
|
523
|
-
- exporting one trivial accessor function per repository/service method
|
|
524
|
-
- converting SQL errors to strings too early
|
|
525
|
-
- bypassing `Migrator` when the project already uses Effect SQL
|
|
526
|
-
- creating ad hoc migration effects without a stable loader shape when `fromRecord(...)` already fits the project
|
|
527
|
-
- scattering migration execution across multiple subsystems instead of one explicit startup boundary
|
|
528
|
-
|
|
529
|
-
## Good Repo Examples To Study
|
|
530
|
-
|
|
531
|
-
- `packages/effect/src/unstable/sql/SqlClient.ts`
|
|
532
|
-
- `packages/effect/src/unstable/sql/Migrator.ts`
|
|
533
|
-
- `packages/effect/src/unstable/sql/SqlResolver.ts`
|
|
534
|
-
- `packages/effect/src/unstable/sql/SqlSchema.ts`
|
|
535
|
-
- `packages/effect/src/unstable/sql/SqlModel.ts`
|
|
536
|
-
- `packages/effect/src/unstable/sql/SqlError.ts`
|