@nanokajs/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eiichiro Iriguchi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,408 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/A-1ro/nanoka/main/assets/images/nanoka-logo.png" alt="nanoka" width="200" />
3
+ </p>
4
+
5
+ # nanoka
6
+
7
+ > Thin wrapper over Hono + Drizzle + Zod for Cloudflare Workers + D1.
8
+ > 80% automatic, 20% explicit.
9
+
10
+ ## What is this
11
+
12
+ Nanoka is a lightweight framework that bridges model definition with DB schema, TypeScript types, and validation. It places the model at the center and derives schema, types, and base validation from it—but keeps API validation as an intentional, explicit adaptation layer, because DB shape and API shape diverge (e.g., `passwordHash` exists in the DB but must not appear in responses).
13
+
14
+ The core idea: stop gluing together Hono + Drizzle + Zod manually. Define a model once, get DB schema, types, and validators automatically. Then customize API behavior explicitly for each route.
15
+
16
+ Targets **Cloudflare Workers + D1 (SQLite)** as first-class. Hono-compatible routing, escape hatch to raw Drizzle, zero magic.
17
+
18
+ ## Status
19
+
20
+ Phase 1 (MVP). Experimental. Expect breaking changes until v1.0.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pnpm add @nanokajs/core hono drizzle-orm zod
26
+ ```
27
+
28
+ Peer dependencies: `hono ^4.0.0`, `drizzle-orm ^0.45.0`, `zod ^3.23.0`, `@cloudflare/workers-types ^4.20240925.0` (for Workers).
29
+
30
+ ## Quickstart
31
+
32
+ ### 3 commands to a working API
33
+
34
+ 1. **Define your model**
35
+
36
+ `src/models/user.ts`:
37
+ ```ts
38
+ import { t } from '@nanokajs/core'
39
+
40
+ export const userTableName = 'users'
41
+ export const userFields = {
42
+ id: t.uuid().primary(),
43
+ name: t.string(),
44
+ email: t.string().email(),
45
+ passwordHash: t.string(),
46
+ createdAt: t.timestamp().default(() => new Date()),
47
+ }
48
+ ```
49
+
50
+ 2. **Generate Drizzle schema**
51
+
52
+ Create `nanoka.config.ts` in your project root:
53
+ ```ts
54
+ import { defineConfig } from '@nanokajs/core/config'
55
+ import { userTableName, userFields } from './src/models/user'
56
+
57
+ export default defineConfig({
58
+ models: [
59
+ { name: userTableName, fields: userFields },
60
+ ],
61
+ output: './drizzle/schema.ts',
62
+ })
63
+ ```
64
+
65
+ Then:
66
+ ```bash
67
+ npx nanoka generate
68
+ ```
69
+
70
+ ✓ Creates `drizzle/schema.ts` from your models.
71
+
72
+ 3. **Apply migrations and deploy**
73
+
74
+ ```bash
75
+ npx drizzle-kit generate
76
+ npx wrangler d1 migrations apply <DATABASE> --local
77
+ ```
78
+
79
+ ✓ SQL migrations generated from Drizzle schema. Ready to deploy.
80
+
81
+ ### Minimal example (model + 1 route)
82
+
83
+ `src/index.ts`:
84
+ ```ts
85
+ import { d1Adapter, nanoka } from '@nanokajs/core'
86
+ import { userFields, userTableName } from './src/models/user'
87
+
88
+ export interface Env {
89
+ DB: D1Database
90
+ }
91
+
92
+ export default {
93
+ async fetch(req: Request, env: Env, ctx: ExecutionContext) {
94
+ const app = nanoka(d1Adapter(env.DB))
95
+ const User = app.model(userTableName, userFields)
96
+
97
+ app.get('/users/:id', async (c) => {
98
+ const id = c.req.param('id')
99
+ const user = await User.findOne(id)
100
+ if (!user) return c.notFound()
101
+ return c.json(user)
102
+ })
103
+
104
+ app.post(
105
+ '/users',
106
+ User.validator('json', { omit: ['id', 'passwordHash', 'createdAt'] }),
107
+ async (c) => {
108
+ const body = c.req.valid('json')
109
+ const user = await User.create({
110
+ ...body,
111
+ id: crypto.randomUUID(),
112
+ passwordHash: 'hashed_value_here', // use bcrypt, argon2, etc. in production
113
+ createdAt: new Date(),
114
+ })
115
+ return c.json(user, 201)
116
+ }
117
+ )
118
+
119
+ return app.fetch(req, env, ctx)
120
+ },
121
+ }
122
+ ```
123
+
124
+ ### Full working example
125
+
126
+ Complete CRUD with curl tests, deployment steps, and troubleshooting:
127
+ [examples/basic/README.md](../../examples/basic/README.md)
128
+
129
+ ## How it fits with drizzle-kit and wrangler
130
+
131
+ ### Migration flow
132
+
133
+ ```
134
+ User model definition
135
+ (userFields + userTableName)
136
+ |
137
+ v
138
+ nanoka generate
139
+ |
140
+ v
141
+ drizzle/schema.ts (Drizzle schema code)
142
+ |
143
+ v
144
+ drizzle-kit generate
145
+ |
146
+ v
147
+ drizzle/migrations/*.sql (SQL migrations)
148
+ |
149
+ v
150
+ wrangler d1 migrations apply
151
+ |
152
+ v
153
+ D1 Database
154
+ ```
155
+
156
+ **Nanoka generates Drizzle schema code only.** Nanoka does NOT generate SQL, does not compute diffs, does not apply migrations. That responsibility stays with `drizzle-kit` (diff + SQL) and `wrangler` (apply).
157
+
158
+ This design keeps Nanoka thin and lets the stable Drizzle ecosystem handle migration machinery. You own the SQL files; migrations are human-reviewable and version-controlled.
159
+
160
+ ## Core API
161
+
162
+ ### Model definition with `t`
163
+
164
+ Fields are built with the `t` builder:
165
+
166
+ ```ts
167
+ import { t } from '@nanokajs/core'
168
+
169
+ const fields = {
170
+ id: t.uuid().primary(),
171
+ name: t.string(),
172
+ email: t.string().email(),
173
+ passwordHash: t.string(),
174
+ active: t.boolean().default(() => true),
175
+ age: t.integer().min(0).max(150),
176
+ metadata: t.json(), // { key: string, value: unknown }[]
177
+ createdAt: t.timestamp().default(() => new Date()),
178
+ updatedAt: t.timestamp().default(() => new Date()),
179
+ }
180
+ ```
181
+
182
+ Supported field types:
183
+ - `t.string()` / `t.uuid()`
184
+ - `t.integer()` / `t.number()`
185
+ - `t.boolean()`
186
+ - `t.timestamp()`
187
+ - `t.json()`
188
+
189
+ Modifiers:
190
+ - `.primary()` — primary key (required, one per model)
191
+ - `.unique()` — unique constraint
192
+ - `.optional()` — nullable
193
+ - `.default(fn)` — default value (function or constant)
194
+ - `.min(n)` / `.max(n)` — numeric / string length bounds
195
+ - `.email()` — email validation (string modifier: `t.string().email()`)
196
+
197
+ ### `schema()` vs `validator()` (two methods, not one)
198
+
199
+ This intentional separation keeps validation library swappable for Phase 2.
200
+
201
+ **`schema(opts)` — pure Zod schema, standalone**
202
+
203
+ ```ts
204
+ const CreateSchema = User.schema({ omit: ['id', 'passwordHash', 'createdAt'] })
205
+ const parsed = CreateSchema.parse(data) // use anywhere Zod is needed
206
+ ```
207
+
208
+ **`validator(target, opts, hook?)` — Hono validator**
209
+
210
+ ```ts
211
+ app.post('/users', User.validator('json', { omit: ['id', 'passwordHash'] }), handler)
212
+ // or with error hook:
213
+ app.post('/users', User.validator('json', opts, (result, c) => {
214
+ if (!result.success) return c.json({ error: 'Invalid request' }, 400)
215
+ }), handler)
216
+ ```
217
+
218
+ The `hook` parameter is optional. If you omit it, `@hono/zod-validator` returns issues in the response (see Error handling section for hardening).
219
+
220
+ Options apply to both:
221
+ - `pick: ['name', 'email']` — include only these fields
222
+ - `omit: ['passwordHash']` — exclude these fields
223
+ - `partial: true` — all fields optional (good for PATCH)
224
+
225
+ ### `findMany` requires `limit` (type-safe pagination)
226
+
227
+ `limit` is a required parameter—omitting it is a **compile error**:
228
+
229
+ ```ts
230
+ // @ts-expect-error Missing required property 'limit'
231
+ const users = await User.findMany()
232
+
233
+ // OK
234
+ const users = await User.findMany({ limit: 20 })
235
+ const users = await User.findMany({ limit: 20, offset: 10 })
236
+ const users = await User.findMany({ limit: 20, orderBy: 'id' })
237
+ ```
238
+
239
+ This prevents accidental unbounded queries. Pagination shape: `{ limit, offset?, orderBy? }`.
240
+
241
+ ## Escape hatch: raw Drizzle and D1 batch
242
+
243
+ ### `app.db` — use raw Drizzle when needed
244
+
245
+ When the typed API doesn't fit, drop down to `app.db` (raw Drizzle):
246
+
247
+ ```ts
248
+ import { eq } from 'drizzle-orm'
249
+
250
+ // Raw select with explicit types
251
+ const users = await app.db
252
+ .select()
253
+ .from(User.table)
254
+ .where(eq(User.table.email, 'alice@example.com'))
255
+
256
+ // Complex joins, subqueries, raw SQL
257
+ const result = await app.db
258
+ .select()
259
+ .from(User.table)
260
+ .innerJoin(Post.table, eq(User.table.id, Post.table.userId))
261
+ ```
262
+
263
+ `User.table` is the underlying Drizzle table. Combine with `eq()`, `lt()`, `and()`, `or()` from `drizzle-orm` for complex conditions. SQL injection is prevented by Drizzle's parametrized bindings.
264
+
265
+ ### `app.batch()` — D1 batch API directly
266
+
267
+ For transactions or batched queries, use `app.batch()` (D1's native batch):
268
+
269
+ ```ts
270
+ const results = await app.batch([
271
+ app.db.insert(User.table).values({ ... }),
272
+ app.db.update(User.table).set({ ... }).where(...),
273
+ ])
274
+
275
+ // D1 batch returns results in order
276
+ const [insertResult, updateResult] = results
277
+ ```
278
+
279
+ No custom transaction abstraction. Nanoka exposes D1's `batch()` directly. Isolation level and rollback behavior match D1 docs.
280
+
281
+ ## Error handling
282
+
283
+ ### `HTTPException` (Hono standard)
284
+
285
+ Nanoka uses Hono's `HTTPException` for errors:
286
+
287
+ ```ts
288
+ import { HTTPException } from 'hono/http-exception'
289
+
290
+ app.get('/users/:id', async (c) => {
291
+ const user = await User.findOne(c.req.param('id'))
292
+ if (!user) throw new HTTPException(404, { message: 'Not found' })
293
+ return c.json(user)
294
+ })
295
+ ```
296
+
297
+ ### Hardening Zod validator errors
298
+
299
+ By default, `@hono/zod-validator` returns the full `issues[]` array in the response, which leaks your API schema shape (field names, types, constraints) to attackers during reconnaissance. Default zod-validator behavior leaks the API schema shape via `issues[]`. Replace with a fixed string in production.
300
+
301
+ **Option 1: validator hook (recommended)**
302
+
303
+ Pass a hook as the third argument to `User.validator()`. The hook receives the validation result and the Hono context. When you return a response from the hook, it replaces the default behavior:
304
+
305
+ ```ts
306
+ app.post(
307
+ '/users',
308
+ User.validator('json', { omit: ['passwordHash'] }, (result, c) => {
309
+ if (!result.success) return c.json({ error: 'Invalid request' }, 400)
310
+ }),
311
+ handler
312
+ )
313
+ ```
314
+
315
+ **Option 2: `app.onError` (for non-validator paths)**
316
+
317
+ `@hono/zod-validator` does not throw on validation failure — it calls `c.json(result, 400)` directly, so `app.onError` does not receive validator failures. Use `app.onError` for cases where you call `User.schema().parse(data)` directly or write middleware that throws `ZodError`:
318
+
319
+ ```ts
320
+ import { ZodError } from 'zod'
321
+
322
+ app.onError((err, c) => {
323
+ if (err instanceof ZodError) {
324
+ return c.json({ error: 'Invalid request' }, 400)
325
+ }
326
+ // ... handle other errors
327
+ })
328
+ ```
329
+
330
+ Note: this handler will not be called for validation failures originating from `User.validator()`. Use Option 1 for those.
331
+
332
+ ## Phase 1 scope: what is NOT included
333
+
334
+ Nanoka Phase 1 is intentionally minimal. These features are Phase 2 or later:
335
+
336
+ - **Relations** (`hasMany()`, `belongsTo()`, lazy loading) — Phase 2. Use raw Drizzle for joins.
337
+ - **Field accessor API** (`User.schema({ pick: f => [f.name, f.email] })`, `User.where(f => eq(f.email, x))`) — Phase 2. Phase 1 uses string arrays and object-form `where`. When Phase 2 lands, the `f` object will be `as const`, with zero runtime cost.
338
+ - **OpenAPI generation** — Phase 2 or 3.
339
+ - **Turso / libSQL adapters** — Phase 2. Nanoka is designed adapter-first; additional adapters will follow.
340
+ - **CLI scaffolder** (`create-nanoka-app`) — Phase 3.
341
+ - **Auth, full-stack React, complex query DSL** — explicitly out of scope at every phase.
342
+
343
+ For relations and complex joins in Phase 1, use raw Drizzle via `app.db`. The escape hatch is always open.
344
+
345
+ ## Workspace structure (for contributors)
346
+
347
+ ```
348
+ packages/nanoka/ # Library
349
+ src/
350
+ field/ # DSL builder (t)
351
+ model/ # Model definition, schema(), validator()
352
+ router/ # nanoka() router, CRUD methods
353
+ adapter/ # Adapter interface, d1Adapter()
354
+ cli/ # CLI entry point, nanoka generate
355
+ codegen/ # Schema code generator
356
+ dist/ # Built ESM + types (not in repo, generated by tsup)
357
+ __tests__/ # Integration tests
358
+ README.md # This file
359
+ LICENSE # MIT
360
+ package.json
361
+ tsup.config.ts
362
+ vitest.config.ts
363
+ vitest.node.config.ts
364
+ tsconfig.json
365
+
366
+ examples/basic/ # Cloudflare Workers example
367
+ src/models/user.ts
368
+ src/index.ts
369
+ drizzle/schema.ts # Generated by nanoka generate
370
+ drizzle/migrations/ # Generated by drizzle-kit generate
371
+ README.md
372
+ wrangler.toml
373
+ ```
374
+
375
+ ## Release checklist (for maintainers)
376
+
377
+ Before running `pnpm publish`:
378
+
379
+ 1. **Run `pnpm publish --dry-run` and verify tarball contents**
380
+ ```bash
381
+ pnpm -C packages/nanoka publish --dry-run
382
+ ```
383
+ Expected in Tarball Contents:
384
+ - `dist/index.js`, `dist/index.d.ts`, `dist/*.js`, `dist/*.d.ts`
385
+ - `LICENSE`
386
+ - `package.json`, `README.md`
387
+
388
+ NOT included (confirm absence):
389
+ - `src/`, `__tests__/`, `.test.ts`, `.spec.ts`
390
+ - `tsconfig.json`, `vitest.config.ts`, `tsup.config.ts`, `scripts/`
391
+ - `node_modules/`
392
+
393
+ Note: `../../examples/basic` is a relative link in this README. It resolves correctly on GitHub but not on the npm registry. The link is kept for GitHub usability.
394
+
395
+ 2. **Verify publish access**
396
+ ```bash
397
+ npm view @nanokajs/core 2>&1 | grep E404
398
+ ```
399
+ Confirm E404 (not yet published). Confirm that the `nanokajs` npm org has been created and you have publish rights to `@nanokajs/core`.
400
+
401
+ 3. **Publish** (prepublishOnly hook will run build + test + typecheck again)
402
+ ```bash
403
+ pnpm -C packages/nanoka publish
404
+ ```
405
+
406
+ ## License
407
+
408
+ MIT — see LICENSE file.